How to fill a bath

by Dermot Tynan in Control Theory

Posted on Tuesday, April 03, 2018 at 08:30

Bath Taps

It seems straightforward enough to fill a bath. We know how to do it instinctively, but how do we program a computer to do it?

Let’s start with some assumptions. Assume we have a hot and cold tap, and we can control the flow on each tap. Also assume we can measure the average temperature of the water in the bath tub. Finally, assume we can measure the height of water in the bath. This last one is so we know when we’re done.

We’ll denote t as the measured temperature and l as the measured height of water. For that matter, we can refer to h as the amount of hot water, where 0.0 ≤ h ≤ 1.0 and c as the amount of cold water, again in the range 0.0 ≤ c ≤ 1.0.

Ultimately, we’re going to use a PID Controller to do the actual hard work of adjusting the taps. But before we get into that, we need to refine our equations just a bit, and also learn how to walk before we try to start running. (See what I did there? Running? Bath? Oh, never mind).

Let's imagine that the hot tap puts out 60°C and the cold tap puts out 10°C. Under these conditions, the coldest temperature we can achieve for our bath is 10.0°C. Similarly, the maximum temperature is 60.0°C. We can decide to express t as a rational number in the range 0.0 ≤ t ≤ 1.0, such that the aggregate temperature of the bath is 50.0t + 10.0. Keeping t bounded in the 0.0 to 1.0 range makes our life a bit easier, and we can figure out the actual temperature as needed.

Lets also create a variable u which represents the aggregate temperature coming out of the hot and cold taps. Again, a rational in the range 0.0 to 1.0. We can write this aggregate temperature u in terms of the hot and cold taps as follows: u = h / (h + c). Also, if we want to know what that is as an actual temperature value, and assuming the same hot and cold tap temperatures, the actual output temperature from the taps is U°C = 50.0u + 10.0.

u and t are similar, in a sense. If we set u to 0.5 for the entire bath fill, then t would also end up as 0.5. But if after half-filling the bath, we slide the value of u to 0.75, t will never catch up because it is the aggregate bath temperature, so will always include the half-full amount of water at 0.5. The rate at which we fill the bath can be expressed as r = (h + c) / 2. Again this is a rational number. We can similarly think of the bath as being full when the level l is equal to 1.0 and empty when it is equal to 0.0.

So, why are we interested in r, l, t and u? Ultimately, we can control the hot and cold taps. But we want to determine those values in terms of r and u. Using some simple variable substitution, we can deduce that h = 2ru and c = 2r(1 - u).

So our inputs are l and t and we want to feed these into an algorithm which gives us r and u which we can then translate into values for the taps.

If you study those formulae for a little bit, it's obvious that for large values of r we have little control over the resultant temperature. We may need to add code to make sure that h and c never exceed 1.0 but for now, it is sufficient to make sure that r doesn't exceed 0.5.

We can start off by setting r = 0.5 for the first little bit as we start putting water into the bath tub. When the bath tub is 80% full, we can back off the value of r until it reaches 0.0 when l1.0. We can also declare victory and complete the program when l1.0.

The only thing left is for us to figure out a mechanism for determining the value of u. We need some ideal temperature, which we will call T. In PID terms, this is known as the setpoint. This might be, for example, 0.8 (or 50°C using our earlier real-world example). We need a way to compute the error, which could be written as e = T - t. In other words, the difference in temperature between what we want the bath temperature to be, and what the temperature actually is. We want the error (e) to be as close to zero as possible. (Hint: This is what the PID Controller will do for us).

A simple way to set the tap values, would be to set u = T. As long as no outside influences affect our system, this will fill the entire bath with water of temperature T. But the reality is that the water from the hot and cold taps might not always be at our defined temperature. The individual flow rates from the taps might not be entirely identical. The bath itself will begin cooling down as soon as we start pouring water into it. In other words, it is rare for there to be no outside influences.

Here's a simple version of the program, written in Ruby:

def fill_bath(desired_temp)
  level = 0.0
  uvalue = 0.5
  while level < 1.0
    level = read_bath_level()
    actual_temp = read_bath_temp()
    if level <= 0.8
      rate = 0.5
      rate = 0.5 - (level - 0.8) * 2.0
    error = desired_temp - actual_temp
    uvalue += (error * 0.1)
    uvalue = [uvalue, 1.0].min
    uvalue = [0.0, uvalue].max
    hot_tap = [1.0, 2.0 * rate * uvalue].min
    cold_tap = [1.0, 2.0 * rate * (1 - uvalue)].min
    set_taps(hot_tap, cold_tap)

As you can probably tell, it's not particularly brilliant. The external functions are obvious from their names. We pick an initial value for u of 0.5. After each measurement, we adjust u up or down based on the computed error between what we want the water temperature to be, and what it actually is. Positive values for the error e mean we need more hot water, and vice versa. Once the water level is at the right height, the function completes.

With a certain amount of tuning, particularly in terms of the speed of execution of the loop, we can make the algorithm work. But try this for size: Imagine that we can sample the water temperature quite quickly, and likewise sample the water height. In that type of environment, we can expect the u value to saturate, or hit 1.0 quite quickly, assuming the water is too cold. As a result, we will end up shutting off the cold tap, and pouring in nothing but hot water. As the measured temperature stabilizes around our desired temperature, the error value will start to swing negative. Again, fairly quickly, we will "bottom out" and our u value will reach 0.0. As a result, we will shut off the hot tap completely, and fully turn on the cold tap. This is the basis of control theory, by the way. We have an oscillation in our control. We could add a sleep(10) to the code to slow down the loop and damp out the oscillation.

But how do we know that ten seconds is sufficient? We need to understand the properties of our control points. A simple way to achieve this would be to log the error values over the fill time, and graph them. Ideally, the error value would be zero, or at a minimum, would oscillate around zero with a small amplitude. Massive disparities in the error value tell us that our solution is not tuned to the real-world conditions of the bath tub. A sleep() may indeed fix this, but then again, it may not.

In a later article, I will explain PID controllers and how they can help with situations such as this.

Upcoming Missions
Recent Posts