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:
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:
The equation has a few new concepts:
- is the instantaneous error.
- is the proportional gain - how hard do we want to push for a given amount of error?
- is the integral time constant - often called the integral reset time2. It represents how patient we want the integral term to be. The integral action exists to address steady-state error, but a linear system takes time to present that error.
“What’s error?”
In our case error is the current gap to the target temperature: . Notice that using error is a very neat mechanism to incorporate the potential effects of changing the input variables:
- Changed ? Error changes.
- changed? The boiler now bleeds heat at a different rate, so it settles somewhere different with an error that doesn’t close.
“Cool! So… how do we use it?”
Let’s think about the 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:
We’ll assume a cold start () because this makes the derivation simpler3. Our identities then simplify to just:
Substituting into the error formula lets us rephrase :
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 ?…)
“Keep ’er steady!”
The steady part is more difficult because depends on things that are no longer fixed, like . 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:
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 , is the temperature we’ve gained. That’s quite a lovely equivalence, and it makes sense: At the start (), error will exactly equal the gap to , but as we advance and increases, error must shrink to mirror its rise - they’re two views of the same thing!
We already know that and that , so we can get our final equation out:
Again, let’s think about what this produces as time varies. At the start (), the integral part is doing nothing because we’ve not accumulated any error yet, 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:
Note the cancellation happening here too: in the open-loop formula we added 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 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:
- The gain still scales by the same ratio we started with: . This maintains our “want to go times as fast? Use times the flame” rule.
- The integral reset time is just , the boiler’s own time constant, which is perfect. We learned that the integral term exists to close steady-state error, but the boiler itself takes time () to show us that error. If our integral term were to be too impatient, and act more quickly than the boiler can react, we’d introduce instability. Likewise if it was too patient, we’d be sacrificing speed and our controller would be sluggish.
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.