My personal example of using math in programming

I was working on a small personal tool which adjusts the brightness via DDC protocol.

BTW, you can check the Github repository of it.

I wanted the tool which adjusts brightness automatically and gradually, using the time of the day and the season information. The first version which has been created perfectly fit my needs and had been working on my PCs for over 2 years.

Then I decided to share it with public and I started to work on its improvement. My goal was to generalize and parameterize the tool as much as possible, so that it could be used by other people with different requirements. And during this optimization I faced a problem with the brightness adjustment algorithm. Its initial implementation was rigid, built with if-else statements. I struggled with this problem for a while, looking for a fancy built-in Python modules or operators. Until I realized that what I need, is just to make a function. No, not the Python function, but the mathematical function!

Let’s go, I will show you how I did it.

The problem

Let’s say, I have 4 numbers: 6, 10, 17, and 21. And I have two other numbers, let’s say 50 and 100. My goal is to write a Python function, which gradually changes the output number from 50 to 100 and back, based on the input number. And the logic is the following:

  1. If input is less than 6, then return 50.
  2. If input is greater or equal to 6, but less than 6 + 1/3*(10 - 6), then return 50 + 1/3*(100 - 50).
  3. If input is greater or equal to 6 + 1/3*(10-6), but less than 6 + 2/3*(10 - 6), then return 50 + 2/3*(100 - 50).
  4. If input is greater or equal to 10, but less than 17, then return 100.
  5. If input is greater or equal to 17, but less than 17 + 1/3*(21 - 17), then return 50 + 2/3*(100 - 50).
  6. If input is greater or equal to 17 + 1/3*(21 - 17), but less than 17 + 2/3*(21 - 17), then return 50 + 1/3*(100 - 50).
  7. If input is greater or equal to 21, then return 50.

The very quick, naive and inefficient implementation of this function could be like this:

 1def brightness(x):
 2    if x < 6:
 3        return 50
 4    elif 6 <= x < 6 + 1/3*(10 - 6):
 5        return 50 + 1/3*(100 - 50)
 6    elif 6 + 1/3*(10 - 6) <= x < 6 + 2/3*(10 - 6):
 7        return 50 + 2/3*(100 - 50)
 8    elif 10 <= x < 17:
 9        return 100
10    elif 17 <= x < 17 + 1/3*(21 - 17):
11        return 50 + 2/3*(100 - 50)
12    elif 17 + 1/3*(21 - 17) <= x < 17 + 2/3*(21 - 17):
13        return 50 + 1/3*(100 - 50)
14    elif x >= 21:
15        return 50

As you can see, I wrote a lot of if-elif-else statements, very straightforward and easy to understand. It does work. But let’s assume, now there is a need to have 4 steps from 50 to 100 and back. How can I do it?

I cannot add more elif statements. Theoretically I could, but it means moving from one hard-coded solution to another hard-coded solution. But what is needed, is a general solution, which can be easily configured and changed.

The math

Stuck with this conundrum, I decided to take a step back and think about the problem from a different angle. And suddenly I realized, that what I am doing is just trying to interpolate a function.

Let’s recall, what is function in mathematics.

In mathematics, a function from a set X to a set Y assigns to each element of X exactly one element of Y. The set X is called the domain of the function and the set Y is called the codomain of the function. Wikipedia

As we can see, the function is a mapping from one set to another. And in our case, this is what we have. We have set of numbers which must be mapped to another set of numbers. And for a given input number, we must return the corresponding output number.

Let’s draw a graph of the function for a better understanding.

Function graph

Function graph

Some can ask: “Ok, we have a mathematical function, but how it helps us?”

The answer is simple: we can use the mathematical function to interpolate the output number for any input number. Let’s write some Python code for it.

The Python code for our mathematical function

First of all, let’s make a function, which builds intermediate points between two given points. We need this function for flexibility, so that we can change the number of intermediate points -> make changes more gradual if we want to.

Building intermediate points for X axis

This function will build intermediate points between two given points on the X axis.

1def intermediate_points(start, end, steps):
2    if start >= end:
3        raise ValueError("Start must be less than end")
4    if steps < 1:
5        raise ValueError("Number of steps must be greater than 0")
6    if steps == 1:
7        return [start, end]
8    step = (end - start) / (steps - 1)
9    return [start + i * step for i in range(steps)]

Because we must count the start and end points as part of the output, we need to subtract 1 from the number of steps.

Building intermediate points for Y axis

This function will build intermediate points between two given points on the Y axis.

1def interpolate_values(start, end, steps):
2    if steps < 1:
3        raise ValueError("Number of steps must be greater than 0")
4    if steps == 1:
5        return [start, end]
6    step = (end - start) / steps
7    return [ start + (i + 1) * step for i in range(steps) ]

Building the function

We have two supporting functions, now we can build our final function. It will:

  1. Build missing intermediate points for X axis.
  2. Build missing intermediate points for Y axis.
  3. Build pairs of X and Y points (with tuples)
  4. Return the interpolated value for the given input number x.
 1
 2def get_interpolated_value(x_input, x_raise_start, x_raise_end, x_descent_start, x_descent_end, y_min, y_max, steps):
 3    # building intermediate points for X axis
 4    x_points = []
 5    x_points.extend(intermediate_points(x_raise_start, x_raise_end, steps))
 6    x_points.extend(intermediate_points(x_descent_start, x_descent_end, steps))
 7
 8    # building intermediate points for Y axis
 9    y_points = []
10    y_points.extend(interpolate_values(y_min, y_max, steps))
11    y_points.extend(interpolate_values(y_max, y_min, steps))
12
13    # building function mappings using built in zip() function
14    function_points = list(zip(x_points, y_points))
15
16    # Calculating the output value
17    # At first we have to address the edge cases
18    if x_input < x_points[0]:
19        return function_points[0][1]
20    if x_input >= x_points[-1]:
21        return function_points[-1][1]
22
23    # finding the closest point on the left to the input
24    # and returning the corresponding output number
25    return min(function_points, key=lambda p: x_input - p[0])[1]

Here is the image of what this function does:

Interpolation function

Interpolation function

As you can see, with a little bit of math, we have created a function which can be used to interpolate the output number for any input number.

Conclusion

In this post I just wanted to show that sometimes the solution to the problem is not in the Python modules or fancy operators, but in the knowledge of mathematics.

References

If you want to use my DDC brightness adjustment tool, you can find it on External Monitor Brightness tool repo.