Picture yourself stopped at a red light. When it turns green, you need to accelerate to 60kph. While you’re stopped, you can floor it, and the car will accelerate to maximum capacity. But as you’re approaching the target, 60kph, you need to reduce the throttle, or you’ll over-shoot. You fine-tune the speed through a feedback loop, based on your sensation of speed and acceleration, and the tachymeter, until you reach the 60kph.

All of that is natural and mostly unconscious. But operating a car is not as simple as giving the direction you want it to go and the speed you want to achieve. Your controls are the throttle, and the steering. To operate it you need to determine how much to accelerate, and how much to turn the wheel, to get to your goals.

It’s the same when an automate drives. The components helping this kind of control are called controllers. A very frequent kind of controllers is called PID, for Proportional Integral Derivative. Let’s discuss how it works.

And to illustrate that, we are going to simulate a rocket. We’ll use KSP, and try to make that rocket climb at precisely 100m/s.

Rocket

PID

When it comes to accelerating a car, one could think that it’s down to applying a set pressure on the pedal until it’s at its desired speed. This is called an open loop: “I know that applying parameter x will have result y without looking at what happens”. But in the physical world, car weight changes, engine efficiency depends on fuel, and earth is far from flat, and you adjust the throttle accordingly3. When you’re driving, you don’t worry about all that though, you observe speed and adjust throttle.

A system in which you adjust the parameters (input) based on observations (output) depending on your goal (setpoint) is called in a closed loop, and the thing that adjusts input to output is called a controller.

PID stands for Proportional Integral Derivative. It is the physical materialization of a function converting output to input, by summing three terms: a proportional component, an integral component, and a derivative component. It’s used in robots, your car’s cruise control, rockets, manufacture automates, plastic moulding, your vacuum robot, and many other applications. What makes PID so widely used is its universality. In itself, it’s a design pattern that you apply for a specific case.

The things we need to define

  • Setpoint is your measured goal. It can be speed (100m/s), direction (120°E), gravity (1.2g), altitude (10m), etc.
  • Error is the difference between your setpoint and situation at time \(t\). We note that error \(e(t)\) since it depends on time
  • The output of the controller is a term that adjusts (i.e. is added to) the steering. Let’s assume steering is a \([0, 1]\) or a \([-1, 1]\) value.

Proportional

The proportional component ( \(P(t)\) ) is proportional to the error, which means that it’s calculated by multiplying error by a coefficient called the proportional gain ( \(K_p\) ). It is the simplest to understand:

\[P(t) = K_p e(t)\]
  • If your car is stopped and you need to do 60, your error is 60. You can ram it. The proportional term does that: since the error is big1, the output is big.
  • If you car is at 55, error is 5, then proportionally you want to accelerate slowly to fine tune. Proportional does that, since error is small, output is small.

The problem of the proportional term is that, in the case of a car (and assuming you control speed with gas pedal only), it’s going to get you past your desired speed before it stops accelerating. Negative error is going to start to accumulate, then speed is going down, and under your desired speed. At which step throttle is going back up, then stops accelerating once you got beyond desired speed. All in all, you usually obtain an oscillation similar to this one:

This problem is very frequent in automation, and this is precisely where PID is useful.

Integral

So let’s pretend you went beyond you maximum speed. What you need to do now is decrease gas, but not too much either, just so that speed decreases down to the desired speed (or just below, but not too much either). It may take a few oscillations with this method, but in the end it’s going to get you close to your setpoint.

That’s the job of the integral term. It is calculated by integrating the error over time, and multiplying that by the integral gain ( \(K_i\) ). Integrating kind of acts as a memory, it accounts for the total error you had to compensate for.

\[I(t) = K_i \int_0^t e(\tau) \mathrm{d}\tau\]

If you’re unfamiliar with integration, all this does is summing the errors at each point of time, over time. The goal here is to know how much you had to correct in total, in order to level out the steering. For example, if error is 5m/s at \(t_0\) and error is 5m/s at \(t_0 + 1s\), then the integral at \(t_0 + 1s\) is going to be 5m (since error staid at 5m/s for 1s), which represents the distance of the error over 1s.

Derivative

If your speed is stable, but suddenly there is a slope, speed goes down, and you’ll accelerate to conserve speed. The proportional term will give very little variation until the error is actually noticeable. The integral term will also stay relatively constant, since it accumulates error, and error is small in this situation. This case is handle by the derivative term.

The derivative term accounts for the speed of variation of the error, that is: the rate at which it is created or corrected. In our scenario, speed starts to decrease, even though it didn’t change much yet, the derivative term will give positive or negative results to correct the unnoticeable yet present error.

\[D(t) = K_d \frac{\mathrm{d}e(t)}{\mathrm{d}t}\]

If you’re unfamiliar with derivation, all this does is measuring what changed in a small period of time, i.e. how \(e(t)\) is varying. For example, if error at \(t_0\) is 5m/s and error at \(t_0 + 1s\) is 15m/s, then the derivative is going to be 10m/s2, which represents the acceleration of the error.

Summing

PID controller’s output is

\[u(t) = P(t) + I(t) + D(t) = K_p e(t) + K_i \int_0^t e(\tau) \mathrm{d}\tau + K_d \frac{\mathrm{d}e(t)}{\mathrm{d}t}\]

You’ll just invoke that every now and then, and use the output to correct your steering.

Back to our rocket

To implement PID, we’ll use the KOS extension, which lets you code and automate your launches.

Our target is to shoot that rocket, then stabilize it at 100m/s2.

lock STEERING to HEADING(90, 90).       // Shoot straight up

set SETPOINT to 100.                    // This is our target

set KP to 0.                            // We'll need to set these
set KI to 0.                            // to something later.
set KD to 0.

// Lock acts kind of like a function, which means that ERROR is
// going to be re-evaluated for every statement that refers to it.
lock ERROR to (SETPOINT - SHIP:VELOCITY:SURFACE:MAG).
set PREVIOUS_ERROR to ERROR.            // We need that to calculate d(e(t))
set T0 to TIME:SECONDS.                 // We need that to calculate d(t)
set P to 0.
set I to 0.
set D to 0.
set THROTT to 1.                        // Start by ramming it.
stage.                                  // This launches the rocket

lock THROTTLE to THROTT.                // This sets the actual throttle to our
                                        // variable

until SHIP:LIQUIDFUEL < 0.1 {           // We stop when there's no more fuel
  wait 0.01.                            // Every 10ms
  set DT to TIME:SECONDS - T0.
  set P to KP * ERROR.                  // Calculate P
  set I to KI * (I + ERROR * DT).       // Calculate I
  set D to KD * (ERROR - PREVIOUS_ERROR) / DT. // Calculate D

  set THROTT to THROTT + P + I + D.     // Adjust throttle

  set T0 to TIME:SECONDS.               // Update dt and de(t)
  set PREVIOUS_ERROR to ERROR.          
}.

Now we need to set \(K_p\), \(K_i\) and \(K_d\). To do so, we’ll use a method called Ziegler–Nichols. The principle is to find a “stable” proportional gain, then calculate integral and differential gain from pre-established values.

We have an error of 100m/s to begin with, the function is going to be called roughly every 10ms, so we can try something in the likes of 0.0005 - it’s sufficiently small for fine tuning, and sufficiently big to change throttle fast. We set:

\[K_p = 0.0005\] \[K_i = 0\] \[K_d = 0\]

and shoot. I also added some form of logging to observe what’s happening:

.0005

Not too bad, we have stable oscillations (this is called marginal stability) around 100m, period is long, but the amplitude is a bit high (36m or so), lets check if we can do better. \(K_p = 0.005\)

.0005

A bit more oscillations (around 3.5s or so), still they’re not too frequent, and amplitude is much more contained. That is a good candidate.

Let’s try \(K_p = 0.05\), and shoot.

.0005

Amplitude didn’t change that much, but oscillations went way worse. We are set for \(K_p = 0.005\) as our ultimate \(K_p\), also called \(K_u\). We use Ziegler-Nichols, which tells us to set variables accordingly:

\(K_p\): \(0.6 K_u\)

\(K_i\): \(1.2 \frac{K_u}{T_u}\) (\(T_u\) is the oscillation period)

\(K_d\): \(0.075 K_u T_u\)

In our code, that is:

set KP to 0.003.                        
set KI to 0.004545455.                  
set KD to 0.000495.

Then shoot:

PDI

Seems to be working well. Now the graph:

PDI

We see the stabilization of speed at 100m/s, happening in something around 10s. It’s probably possible to do something better by tuning \(K_p\). We do have some oscillation at the beginning, that are quickly absorbed. Once stabilized, throttle goes gently down as fuel gets depleted (which our differentiate is absorbing).

There you go, PID controller explained and demonstrated.

More on the topic

Notes

  • 3 the issue is coming from the fact that your actuators are impacting acceleration and turn rate, which are one order of derivation away from the variables you want to control: speed and direction.
  • 1 small error or large error entirely depend on your parameters. If \(output \in [0,1], Kp = 1\), an error of 1 is big, since it would change your steering completely.
  • 2 there’s an existing pid function in KOS to spare you all of that.