Root finding algorithms

by Eugeniy E. Mikhailov and Greg Bentsen

Logistics and Agenda

Homework 3 due Monday Sept 21 at 11:59pm

Agenda

Today

Midterm 1:

Do it at home part

In class part

Root finding problem

Definition and canonical form

We want to find root of equation (x0x_0) which satisfies

f(x0)=0f(x_0) = 0

Often it would work a bit different

h(x0)=g(x0)h(x_0) = g(x_0)

Which can be moved to canonical form

f(x0)=h(x0)−g(x0)=0f(x_0) = h(x_0) - g(x_0) = 0

Example

2sin(x0)=1→2sin(x0)−1=02\sin(x_0) = 1 \to 2\sin(x_0) - 1 = 0

Trial and error method

One can try to get the solution by just guessing with a hope to hit the solution. This is not highly scientific.

However, each guess can provide some clues.

A general algorithm

Bisection method pseudocode

Works for any continuous function in vicinity of a function root

  • make initial bracket for search, i.e. set x+x_+ and x−x_- such that
    • f(x+)>0f(x_+) >0
    • f(x−)<0f(x_-) <0
  • loop begins
    • make the new guess value xg=(x++x−)/2x_g=(x_+ + x_-)/2
    • if |f(xg)|≤εf|f(x_{g})| \le \varepsilon_f and |x+−xg|≤εx|x_{+}-x_{g}| \le \varepsilon_x
      then stop, we found the solution with the desired precision
    • otherwise if f(xg)>0f(x_{g})>0 then x+=xgx_+=x_{g} else x−=xgx_-=x_{g}
    • continue the loop
Bisection method illustration

Bisection python implementation

AI uses prompt in bisection_ai_assignment.txt

#!/usr/bin/env python3
"""
Bisection method implementation.

The function bis(f, x_m, x_p) finds a root of f in the interval [x_m, x_p]
assuming f(x_m) < 0 and f(x_p) > 0.

Parameters:
    f: callable, function whose root is to be found.
    x_m: float, lower bound of the bracket (f(x_m) < 0).
    x_p: float, upper bound of the bracket (f(x_p) > 0).
    eps_f: float, tolerance for function value (default 1e-6).
    eps_x: float, tolerance for interval width (default 1e-6).

Returns:
    x0: float, approximation of the root.
    f(x0): float, function value at the root approximation.
"""

def bis(f, x_m, x_p, eps_f=1e-6, eps_x=1e-6):
    """Bisection method to find a root of f in the bracket [x_m, x_p].

    The algorithm iteratively narrows the bracket until both the function
    value and the bracket width are within the specified tolerances.

    Args:
        f: callable, function whose root is to be found.
        x_m: float, lower bound of the bracket (f(x_m) < 0).
        x_p: float, upper bound of the bracket (f(x_p) > 0).
        eps_f: float, tolerance for function value (default 1e-6).
        eps_x: float, tolerance for interval width (default 1e-6).

    Returns:
        x0: float, approximation of the root.
        f(x0): float, function value at the root approximation.
    """
    x_m= x_m
    x_p= x_p
    while True:
        x_g = (x_p+ x_m) / 2.0
        f_g = f(x_g)
        if abs(f_g) <= eps_f and abs(x_p- x_g) <= eps_x:
            return x_g, f_g
        if f_g > 0:
            x_p= x_g
        else:
            x_m= x_g

if __name__ == "__main__":

# Simple test: find sqrt(2) as root of f(x) = x^2 - 2

def f(x):
        return x * x - 2.0
    root, val = bis(f, 1.0, 2.0)
    print(f"Root approximation: {root}")
    print(f"f(root) = {val}")
    print(f"x mismatch = {abs(root - 2**0.5)}")

Solution convergence

Solution convergence expression

We say that algorithm has defined convergence if it is possible to express

limk→∞(xk+1−x0)=c(xk−x0)m\lim_{k\to\infty} (x_{k+1}-x_0)=c (x_k-x_0)^m

Where x0x_0 is true root of the equation, cc is some constant, and mm is the order of convergence.

The best algorithms have quadratic convergence, i.e. m=2m=2

Generally the speed of the algorithm is related to its convergence order. However, other factors may affect the speed.

Newton-Raphson method

xi+1=xi−f(xi)f′(xi)x_{i+1}=x_i-\frac{f(x_i)}{f'(x_i)}

Need to provide a starting points x1x_1 and the derivative of the function.

Newton-Raphson method converges quadratically (m=2m=2).

Newton-Raphson method illustration

Numerical derivative of a function

Numerical derivative of a function

Mathematical definition

f′(x)=limh→0f(x+h)−f(x)hf'(x)=\lim_{h \to 0}\frac{f(x+h)-f(x)}{h}

The initial intent is to calculate it at very small hh.

Remember about round off errors

For computers with hh small enough f(x+h)−f(x)=0f(x+h)-f(x)=0.

Let’s be smarter. Recall Taylor series expansion

f(x+h)=f(x)+f′(x)1!h+f″(x)2!h2+⋯f(x+h)=f(x)+\frac{f'(x)}{1!}h+\frac{f''(x)}{2!}h^2+\cdots

So we can see

fc′(x)=f_c'(x)=f(x+h)−f(x)h \frac{f(x+h)-f(x)}{h}=f′(x) = f'(x) +f″(x)2h+⋯ +\frac{f''(x)}{2}h +\cdots

Here computed approximation and algorithm error.

There is a range of optimal hh when both the round off and the algorithm errors are small.

Derivative via Forward Difference (fd)

ffd′(x)=f(x+h)−f(x)h f_{fd}'(x) = \frac{f(x+h)-f(x)}{h}

Algorithm error for small hh

εfd≈f″(x)2h\varepsilon_{fd} \approx \frac{f''(x)}{2} h

This is quite bad since error is proportional to hh.

f(x)=a+bx2f(x) = a+b x^2

f(x+h)=a+b(x+h)2=a+bx2+2bxh+bh2f(x+h) = a+b (x+h)^2 = a + b x^2 + 2 b x h + b h^2

ffd′(x)f_{fd}'(x)=f(x+h)−f(x)h=2bx = \frac{f(x+h)-f(x)}{h} = { 2 b x } +bh + b h

So for small xx, the algorithm error dominates our approximation!

Derivative via Central Difference (cd)

This is much better way (though it use an extra computation for a function)

fc′(x)=f(x+h)−f(x−h)2h f_c'(x) = \frac{f(x+h)-f(x-h)}{2h}

Algorithm error εcd≈f‴(x)6h2\varepsilon_{cd} \approx \frac{f'''(x)}{6}h^2

Root finding Python library

One option is to use root_scalar from scipy.optimize library

def f(x): return (x**3-x-2)
from  scipy.optimize import root_scalar

# braketing method

sol=root_scalar(f, bracket=[1,2])
In [18]: sol.root
Out[18]: 1.5213797068045676

# single point guess

sol=root_scalar(f, x0=2)
In [22]: sol.root
Out[22]: np.float64(1.5213797068045676)