Application: Optimization and Approximation

This chapter includes some examples of using our Python knowledge to solve some challenging problems.

Assume that we have a "nice" function f(x) that we want to find the output of for a specific x, but for some reason we can't actually compute f(x). If we know the output of f for some input a, and if we can continously take the derivative of f with respect to x, then we can use the Taylor series expansion of f(x) to approximate that value.

Let's approximate a nice function f at point x using the Taylor series:

$$ f(x) = f(a) + \frac{f'(a)}{1!}(x-a) + \frac{f''(a)}{2!}(x-a)^2 + \frac{f'''(a)}{3!}(x-a)^3 + ... $$

where f is a simple, and continuous function that we can continuously differentiate to get the next terms. Normally, the Taylor series goes on forever, but generally we only compute a fixed number of terms because that is a good enough approximation for us.

To simplify things, let us assume that f(x) is a polynomial, so that the differentiation is easy. Polynomials are made of terms that are powers of $x$.

A term is just something like $a x^{n}$, where $a$ is a coefficient and $n$ represents the power of $x$. Its main use is twofold: A term is easily derivable, and a term needs to be able to return a value for a substition for $x$. .derive_many(num) is just a shorthand for applying .derive() in a loop.

Here are some examples of using this term class:

As we see, the derivative of a constant polynomial is zero.

Given Term, a Polynomial is just a sum of different terms. We technically need to have a unique Term for each power, but this definition is enough for now:

Writing .derive() and .subst() are easy, because the main task is just calling .derive() and .subst() of the Terms inside.

Let's try it out in action:

Now that our polynomial is ready, we can code the rest of the Taylor approximation code as follows:

Looks like it works well. Let's also draw graphs showing the approximation performance of the Taylor series with increasing number of used terms:

It's a bit hard to see, but the blue and purple lines are on top of each other, meaning that the 5-term approximator fits this polynomial perfectly.

Finding a root of a polynomial

A similar application is using iterative refinement to find a root to a polynomial expression that is set to zero. This time, following the explanation in the textbook, the core idea that we need to code is as follows: We start off with a random initial guess for a root ($x_{0}$), and then in a loop we continuously obtain better estimates of a root through this equation:

$$ x_{i+1} = x_{i} - \frac{f(x_{i})}{f'(x_{i})} $$

Let's show an example polynomial that has some "nice" roots:

The parabola above has roots at -5 and 1. The function below is an implementation of the "find-a-root" idea that we talked about.

One detail is that since we're updating our root guess every loop, we would like to stop if we're "stuck" in some value. However, since we're working with floating point numbers, checking for equality is error-prone: instead, we should be checking if our next guess is smaller than our current guess by some fixed margin (set to 0.001 below):

The algorithm is pretty cool because even if our initial guess is very off, the updating mechanism quickly converges to some root of the equation: