PI Control

Last time we learned how to solve linear systems, and by the end we had an elegant equation to represent the required flame strength to heat our boiler to some target temperature:

f(t)=fsteady+fsteady⋅(ττfast−1)⋅e−t/τfastf(t) = f_{steady} + f_{steady} \cdot \left(\frac{\tau}{\tau_{fast}} - 1\right) \cdot e^{-t/\tau_{fast}}

This is called an open-loop solution: the flame is purely a function of elapsed time, and never once looks at how the boiler actually responded. That’s its major drawback, because it only works in a very static, controlled environment. What if the ambient temperature changes? Or the target temperature changes? In reality, lots of these variables can change while the system is running.

We could change our mind about the water temperature, it could be a colder day than usual, or maybe fresh cold water just flowed into the boiler. In this post we’ll take our open-loop solution for the flame, and turn it into something that’s dynamic, and robust to all of these variables.

Proportional control

One of the earliest known applications of proportional control was in “Centrifugal Governors”, which had been regulating the grindstones inside windmills since at least the 18th century1. Geared to the shaft that drove the stones was a pair of heavy flyweights, and as the strength of the wind caused that shaft to spin ever faster, centrifugal force would push the flyweights outwards. The windmill’s grindstone would be connected to the flyweights via a lever, and as the flyweights were pushed outwards the lever would respond by raising the grindstone. This invention allowed the gap between stones to increase proportional to the speed of the wind, helping to prevent wear and grain fires.

The invention was later adopted for the regulation of steam throttle valves in engines during the industrial revolution.

A wild integral appeared!

While proportional control seemed to work well for the windmill, where we have a straightforward mapping from wind speed to grindstone gap, there’s a flaw when you ask this mechanism to hold something steady. The governor on a steam engine is in charge of keeping the engine at a set speed. It does that by connecting the throttle to flyweights: a drop in speed opens the throttle, and an increase closes it. Unfortunately, the flyweights’ action on the throttle, and the speed of the engine controlled by that same throttle, are in a feedback loop which inherently balances out somewhere that isn’t the target speed. To maintain the current engine speed, some throttle is required, but to have the throttle open we need the flyweights to drop, which needs us to not be at the target speed. So, the engine gets stuck in an equilibrium and the distance from that equilibrium to our target is called steady-state error.

The issue of proportional control was revived during the early 20th century when ship steering was solved mathematically. The goal was to keep the ship on course by correcting for outside disturbances, and the solution was conceived initially during observations of ship helmsmen who had learned that they must steer the ship not only based on how off course the ship was right now, but also based on how off course the ship was recently, allowing the helmsman to account for a steady wind pushing the ship off course. The mathematicians observing the helmsman introduced the concept of integral action, which targets exactly that steady gap, by introducing additional corrective action that is proportional to a history of measurements.

A Proportional-Integral Controller (PI controller for short) combines those two actions, and crucially it uses error as its sole input. What we’re going to do in this post is model a closed-loop solution, via a PI controller, on our open-loop formula. By the end our flame equation will arrive at:

f(t)=Kc(e(t)+1τI∫0te(t′)dt′)f(t) = K_c\left(e(t) + \frac{1}{\tau_I}\int_0^t e(t')\,dt'\right)

The equation has a few new concepts:

“What’s error?”

In our case error is the current gap to the target temperature: e(t)=Ttarget−T(t)e(t) = T_{target} - T(t). Notice that using error is a very neat mechanism to incorporate the potential effects of changing the input variables:

“Cool! So… how do we use it?”

Let’s think about the e−t/τfaste^{-t/\tau_{fast}} part of our flame equation, the time-varying term that diminishes with an exponential decay. Okay, well what actually happens to the error as we step through time? Hmm, it also diminishes with an exponential decay… I wonder if we can use error to replace our exponential term?

In doing so we would create a feedback loop which determines the current flame in terms of the current error… that does sound quite robust to changes in our variables.

   T_target
       │ +
       ▼
     ┌───┐ e(t) ┌────────────┐ f(t) ┌──────────┐
     │ Σ │─────▶│ controller │─────▶│  boiler  │
     └───┘      └────────────┘      └──────────┘
       ▲ −                                │
       └────────────── T(t) ──────────────┘

Proportional Boost

We’ll start with the boost part first since it’s a bit easier. Let’s try to replace the exponential term with error…

We can split out the boost term from our main equation as:

fboost(t)=fsteady⋅(ττfast−1)⋅e−t/τfastf_{boost}(t) = f_{steady} \cdot \left(\frac{\tau}{\tau_{fast}} - 1\right) \cdot e^{-t/\tau_{fast}}

We’ll assume a cold start (Tambient=0T_{ambient} = 0) because this makes the derivation simpler3. Our identities then simplify to just:

K⋅fsteady=Ttarget,T(t)=Ttarget⋅(1−e−t/τfast)K \cdot f_{steady} = T_{target}, \quad T(t) = T_{target} \cdot \big(1 - e^{-t/\tau_{fast}}\big)

Substituting T(t)T(t) into the error formula lets us rephrase fboostf_{boost}:

e(t)=K⋅fsteady⋅e−t/τfastfboost(t)=e(t)K⋅(ττfast−1)\begin{align*} e(t) &= K \cdot f_{steady} \cdot e^{-t/\tau_{fast}} \\ f_{boost}(t) &= \frac{e(t)}{K} \cdot \left(\frac{\tau}{\tau_{fast}} - 1\right) \end{align*}

Great! No sign of that pesky exponential term, the boost component of our flame can be defined entirely as a function of error and a bunch of constants4 (wonder if those could be used to define KcK_c?…)

“Keep ’er steady!”

The steady part is more difficult because fsteadyf_{steady} depends on things that are no longer fixed, like TambientT_{ambient}. We know that the PI control solution to this conundrum is to accumulate it instead, so let’s see what happens if we add up all the error we’ve seen so far:

∫0te(t′)dt′=∫0t(Ttarget−T(t′))dt′=∫0tTtarget⋅e−t′/τfastdt′=τfast⋅Ttarget⋅(1−e−t/τfast)T(t)=1τfast⋅∫0te(t′)dt′\begin{align*} \int_{0}^{t} e(t') \,dt' &= \int_{0}^{t} \big(T_{target} - T(t')\big) \,dt' \\ &= \int_{0}^{t} T_{target} \cdot e^{-t'/\tau_{fast}} \,dt' \\ &= \tau_{fast} \cdot T_{target} \cdot \big(1 - e^{-t/\tau_{fast}}\big) \\ T(t) &= \frac{1}{\tau_{fast}} \cdot \int_{0}^{t} e(t') \,dt' \end{align*}

That’s… a lot of maths, but it turns out this is a “great success”. In words, this shows that the error we’ve piled up, scaled by the inverse of τfast\tau_{fast}, is the temperature we’ve gained. That’s quite a lovely equivalence, and it makes sense: At the start (t=0t = 0), error will exactly equal the gap to TtargetT_{target}, but as we advance and T(t)T(t) increases, error must shrink to mirror its rise - they’re two views of the same thing!

We already know that fsteady=TtargetKf_{steady} = \frac{T_{target}}{K} and that e(t)=Ttarget−T(t)e(t) = T_{target} - T(t), so we can get our final equation out:

fsteady=1K⋅τfast⋅∫0te(t′)dt′+e(t)Kf_{steady} = \frac{1}{K \cdot \tau_{fast}} \cdot \int_{0}^{t} e(t') \,dt' + \frac{e(t)}{K}

Again, let’s think about what this produces as time varies. At the start (t=0t=0), the integral part is doing nothing because we’ve not accumulated any error yet, 1Ke(t)\frac{1}{K}e(t) is doing all the work.5 As time advances that term diminishes, but everything it loses is gained by the integral part which eats up our error over time.

Putting it all together

Combining our two results back into one equation yields something distinctly similar to the PI equation from before6:

f(t)=(1K⋅τfast⋅∫0te(t′)dt′+e(t)K)⏟fsteady+e(t)K⋅(ττfast−1)⏟fboost=τK⋅τfast⋅(e(t)+1τ⋅∫0te(t′)dt′)\begin{align*} f(t) &= \underbrace{\left(\frac{1}{K \cdot \tau_{fast}} \cdot \int_{0}^{t} e(t') \,dt' + \frac{e(t)}{K}\right)}_{f_{steady}} + \underbrace{\frac{e(t)}{K} \cdot \left(\frac{\tau}{\tau_{fast}} - 1\right)}_{f_{boost}} \\ &= \frac{\tau}{K \cdot \tau_{fast}} \cdot \left(e(t) + \frac{1}{\tau} \cdot \int_{0}^{t} e(t') \,dt' \right) \end{align*} ∴Kc=τK⋅τfast,τI=τ\therefore K_c = \frac{\tau}{K \cdot \tau_{fast}}, \quad \tau_I = \tau

Note the e(t)K\frac{e(t)}{K} cancellation happening here too: in the open-loop formula we added fsteadyf_{steady} on separately, so the boost term needed to subtract it to avoid counting it twice. In this new formulation, our proportional term hands over the responsibility of fsteadyf_{steady} as time advances. The two equations are equivalent, but the new role assignment is elegant: The integral term holds the ground we’ve already covered, and the proportional term asks for the flame that closes the remaining gap.

Look at what the two constants turned out to be:

While this might at first glance look more complicated than before, we’ve boiled down the flame function to exactly one dependency, error, everything else in the equation is constant.

All 3 posts…