Fixed point iterations and cobwebs

  • In this lecture, we will be looking at solving nonlinear equations.
  • In particular, we will be looking at a very simple idea - fixed point iterations.
    • While fixed point iterations have far reaching consequences for understanding various aspects of nonlinear dynamics as well, it lends an insight into the nature of convergence of roots and the dependency of the convergence behaviour depending on the functional choice.
In [1]:
import numpy as np;
import matplotlib.pyplot as plt;
plt.rcParams.update({"text.usetex":True}); # change inline style to latex
%config InlineBackend.figure_format = "svg" # change figure type to svg

Fixed point iterations in 1D

Let us consider the nonlinear equation $$\exp(x) - 3x^2 = 0, $$ whose roots we are interested it. In order to apply fixed point iterations, we will first cast it in the form of $x = f(x)$. Towards this, we can have multiple possibilities to do so. Let's consider the first possibility as shown below.

  • We rearrange the equation to write $$x = \ln 3 + 2 \ln x.$$ In case we had the root, the left hand side and right hand side of the equations would match exactly. However, if we guess a value different from the root, then the left hand side and right hand side would not match. In that case we can substitute the guess value in the right hand side function; in this case $\ln 3 + 2\ln x$. This value can then be assigned to $x$. This becomes the updated value of $x$.

  • We can program the aforementioned discussion in the following form: $$x^{(k+1)} = f(x^{(k)} $$ where $k$ starts from 0 to $n$, where $n$ is the specified number of iterations. When $k =0$, we are at the first guess point, $x^{(0)}$. There is no universal form for the choice of neither the initial guess nor the number of iterations.

  • Let's first assess how the plot of original function looks like and where to expect the roots to exist.
In [3]:
x = np.linspace(-2,5,200); # create the points between which the root might exist
y = np.exp(x)-3*x**2; 
plt.plot(x, y, label="$f(x) = \exp(x)-3x^2$");  plt.plot(x, 0*x); 
plt.ylim([-2, 2]); 
plt.legend(fontsize=16);
plt.xlabel("$x$", fontsize=16); 
plt.ylabel("$y = f(x)$", fontsize=16)
plt.title("Intersection points of $f(x)$ with X-axis are the roots");
  • Let's take the initial guess value as $x^{(0)} = 2.0$
  • We define the number of iterations and then define as array to store the value of $\textbf{x}$ after each iteration
In [4]:
x0 = 2.0;
Niter = 20;
x = np.zeros(Niter); # Initialization. Stores the value of ith iteration
x[0] = x0;

print("Iteration: %d \t x: %f"%(0,x[0])); # This is for printing the 0th iterate - the intial value

# This function returns the RHS of the equation
def fx(x):
    return np.log(3) + 2*np.log(x);

# This is the loop where the values are updated
# We will print out the iteration number and updated value
for i in np.arange(1,Niter):
    x[i] = fx(x[i-1]);
    print("Iteration: %d \t x: %3.16f"%(i,x[i]));
    
    
# Print the array which contains all the elements
print(x);
Iteration: 0 	 x: 2.000000
Iteration: 1 	 x: 2.4849066497880004
Iteration: 2 	 x: 2.9190824753987616
Iteration: 3 	 x: 3.2411509809654229
Iteration: 4 	 x: 3.4504693041876902
Iteration: 5 	 x: 3.5756327926507705
Iteration: 6 	 x: 3.6468966195075572
Iteration: 7 	 x: 3.6863654180751197
Iteration: 8 	 x: 3.7078942708438589
Iteration: 9 	 x: 3.7195405556699663
Iteration: 10 	 x: 3.7258125969024354
Iteration: 11 	 x: 3.7291822383889315
Iteration: 12 	 x: 3.7309902298311357
Iteration: 13 	 x: 3.7319596398469521
Iteration: 14 	 x: 3.7324792252860597
Iteration: 15 	 x: 3.7327576577037318
Iteration: 16 	 x: 3.7329068464955490
Iteration: 17 	 x: 3.7329867797911067
Iteration: 18 	 x: 3.7330296056332966
Iteration: 19 	 x: 3.7330525500470131
[2.         2.48490665 2.91908248 3.24115098 3.4504693  3.57563279
 3.64689662 3.68636542 3.70789427 3.71954056 3.7258126  3.72918224
 3.73099023 3.73195964 3.73247923 3.73275766 3.73290685 3.73298678
 3.73302961 3.73305255]
  • Let's plot the value of $\textbf{x}$ vs Iteration number
In [4]:
plt.plot(np.arange(0,Niter), x, '-ok'); 
  • It can be clearly seen from the plot above that we start from an initial value of 2 at the zeroth iteration. The values are successively updated and eventually after 15 iterations we converge to the root of the equation. The iterated values of $\textbf{x}$ are also shown above.

Wrapping the above code to see the influence of the guess value

Let us convert that into a function to plot all the family of curves.

  • The function fx(x) takes as an input the value $x$ and returns $\ln 3 + 2\ln x$
  • The function perform_iterations takes as an input the initial guess x_0 and the number of iterations Niter. Based on these values the function will perform the iterations and return the array x which contains all the iterated values.
  • In the end we will pass different values of x_0 to the function and plot the different arrays x.
In [5]:
def fx(x):
    return np.log(3) + 2*np.log(x);

def perform_iterations(x0, Niter):
    x = np.zeros(Niter); # Initialization
    x[0] = x0;
    for i in np.arange(1,Niter):
        x[i] = fx(x[i-1]);
    return x

Niter = 20;
x0_a = np.linspace(1,5, 20);
for x0 in x0_a:
    x = perform_iterations(x0, Niter);
    plt.plot(np.arange(0, Niter), x);

Another rearrangement of the same nonlinear equation

Let us consider the same nonlinear equation $$\exp(x) - 3x^2 = 0$$ but now we will rearrange as $$x = \frac{1}{\sqrt{3}}\exp\frac{x}{2}$$. Let us reuse the same code above

In [5]:
x0 = 2.0;
Niter = 20;
x = np.zeros(Niter); # Initialization
x[0] = x0;

def fx(x):
    return 1/3**(0.5)*np.exp(x/2);

for i in np.arange(1,Niter):
    x[i] = fx(x[i-1]);

print(x);
[2.         1.56940075 1.26540753 1.08697393 0.99419772 0.9491321
 0.92798465 0.91822408 0.9137538  0.91171372 0.91078421 0.91036101
 0.9101684  0.91008075 0.91004087 0.91002272 0.91001447 0.91001071
 0.910009   0.91000822]

We find that this representation, too, converges to a root. In the same way, the third root can also be found by starting with an appropriate initial guess. Try it out yourself!

A case where convergence may not occur.

While the previous nonlinear equation and its rearrangement to a fixed point iteration showed promise, it may not always be the case. Consider the nonlinear equation $$\exp(-x) - 3x = 0$$

  • Before attempting to find out the roots of this equation, let us first look at the curve represented by the equation itself.
In [6]:
x = np.linspace(-2, 4);
y = np.exp(-x) - 3*x;
plt.plot(x,y); plt.plot(x, 0*x);
plt.ylim([-2, 2]);
  • It is clear from the curve above that there does exist a root somewhere between 0 and 1. Somewhere close to 0.25. So our initial guess for the fixed point iterations can be 0.25. Great!

  • Let us set up the equation in the form $x = f(x)$. We can rearrange $$\exp(-x) - 3x = 0\qquad \textrm{as}\qquad x = \frac{1}{3}\exp(-x)$$

  • Let's see what that gives us .

In [7]:
x0 = 2.0;  # Guess value
Niter = 20; # Number of iterations
x = np.zeros(Niter); # Initialization
x[0] = x0; # Setting the initial element of the array x

def fx(x):
    return 1/3*np.exp(-x);

for i in np.arange(1,Niter):
    x[i] = fx(x[i-1]);

print(x, fx(x[Niter-1]));
print(np.isclose(x, fx(x[Niter-1])));
[2.         0.04511176 0.31863021 0.24238146 0.26158559 0.25660999
 0.25788996 0.25756008 0.25764506 0.25762317 0.25762881 0.25762736
 0.25762773 0.25762763 0.25762766 0.25762765 0.25762765 0.25762765
 0.25762765 0.25762765] 0.2576276530512249
[False False False False False False False False False False  True  True
  True  True  True  True  True  True  True  True]

Well, the solution does converge to the value of 0.2576... with the choice of casting the iterations. The speed with which convergence is acheived can be probed with the help of the function np.isclose(x, fx(x[last])) which tells us which elements of x are close to the fx(x[last]). After the first 10 odd terms, we see that we do not have much change in the iterated value of x.

Is convergence always guaranteed?

  • The answer is no. This will be seen from the next case study.
  • In this particular case, we will recast the iterations in the following form: $$\exp(-x) - 3x = 0\qquad \textrm{as}\qquad x = \exp(-x) - 2x$$
  • Let's investigate both the representations together:
In [8]:
x0 = 2.0;
Niter = 20;
x = np.zeros(Niter); # Initialization
xx = np.zeros(Niter);
x[0] = x0;
xx[0] = x0;

def fx(x): # THis function diverges
    return np.exp(-x)-2*x;

def gx(x):
    return 1/3*np.exp(-x); # THis function converges

for i in np.arange(1,Niter):
    x[i] = fx(x[i-1]);
    xx[i] = gx(xx[i-1]);
<ipython-input-8-e419e8d30f3f>:9: RuntimeWarning: overflow encountered in exp
  return np.exp(-x)-2*x;
  • We get overflow error because f(x) diverges
In [9]:
plt.plot(np.arange(0, Niter), x, np.arange(0, Niter), xx)
plt.ylim([-2, 2])
Out[9]:
(-2.0, 2.0)

As seen from the curve of x vs Niter, the orange curve iterates while the blue curve does not converge at all. It should be clear that the choice of the representation leads to the following two observations:

  • The root to which the iterations converge depend on the form of the RHS cast.
  • The iterations may or may not coverge depending on the form of the RHS cast.

Let us investiage this phenomenon further by explicitly plotting how the iterations proceed to actually modify the guess value to the next value. For this, let us consider first the case that showed convergence - $$x = \ln 3 + 2\ln x$$.

We will plot the $f(x)$, which is the RHS of the above iteration scheme. Apart from this we will also plot $x$. The iterations can be logically split into the two steps: $$y_1 = \ln 3 + 2\ln x$$ followed by $$x_1 = y_1$$ hence the two curves give us the way the plots converge.

In [10]:
Niter = 10;
x = np.zeros(Niter);
xg = 1.1;

def fx(x):
    return np.log(3) + 2*np.log(x);

xp = np.linspace(0.2, 5, 100);
yp = fx(xp);
plt.plot(xp, yp, label='$\\ln 3 + 2\\ln x$');
plt.plot(xp, xp, label='$y = x$');
plt.legend(fontsize=16);
plt.xlabel("$x$", fontsize=16);
plt.ylabel("$x, f(x)$", fontsize=16);
plt.xticks(fontsize=16); plt.yticks(fontsize=16);
ax = plt.gca(); ax.set_aspect(0.6);

for i in np.arange(1, Niter):
    xn = fx(xg);
    plt.plot(xg, fx(xg), 'ok', markerfacecolor='black', markersize=2);
    x2 = xn; y2 = x2;
    plt.plot(x2, y2, 'ok', markerfacecolor='black', markersize=2 );
    plt.arrow(xg, fx(xg), x2-xg, y2-fx(xg), length_includes_head=True, linewidth=0.7, color='b');
    plt.arrow(x2, y2, x2-x2, fx(x2)-y2, length_includes_head=True, linewidth=0.7, color='r');
    xg = xn;
    
  • In the above curve, we have started with an initial guess near 1. This eventually iterates towards the root in the vicinity of 3.7.
  • The intersection of the two curves are the roots since at those points of intersection $y = x$ and $y = f(x)$ $\implies$ $x = f(x)$.

  • Interestingly, despite starting the guess near the root near 1, we have coverged to the other root.

  • We did, however, see that the second representation of the nonlinear equation, i.e. $$ \frac{1}{3}\exp\left(\frac{x}{2}\right) $$ does give us the other root.

  • Let's investigate the sequence of convergence for this particular case. It is easy for us to simply reuse the above code block and change the function to the desired one.

In [11]:
Niter = 10;
x = np.zeros(Niter);
xg =3.5;

def fx(x):
    return 1/3**(0.5)*np.exp(x/2);

xp = np.linspace(0.2, 5, 100);
yp = fx(xp);
plt.plot(xp, yp, label='$\\frac{1}{3}\\exp\left(\\frac{x}{2}\\right)$');
plt.plot(xp, xp, label='$y = x$');
plt.legend(fontsize=16);
plt.xlabel("$x$", fontsize=16);
plt.ylabel("$x, f(x)$", fontsize=16);
plt.xticks(fontsize=16); plt.yticks(fontsize=16);
ax = plt.gca(); ax.set_aspect(0.6);

for i in np.arange(1, Niter):
    xn = fx(xg);
    plt.plot(xg, fx(xg), 'ok', markerfacecolor='black', markersize=2);
    x2 = xn; y2 = x2;
    plt.plot(x2, y2, 'ok', markerfacecolor='black', markersize=2 );
    plt.arrow(xg, fx(xg), x2-xg, y2-fx(xg), length_includes_head=True, linewidth=0.7, color='b');
    plt.arrow(x2, y2, x2-x2, fx(x2)-y2, length_includes_head=True, linewidth=0.7, color='r');
    xg = xn;
    
  • This particular sequence does depict convergence towards the other root. This happens despite choosing an initial guess quite close to the other root.
  • If we compare the two curves, it is clear that in one case the curvature is concave down while in the other it is concave up. Regardless of that, the important thing is the slope of the curve at the root. In the slope of $f(x)$ at the root $X$ is such that $|df/dx|_X|<1$, then the iterations converge towards that root. This explains why deciding the shape of $f(x)$ has such a critical influence on the direction of convergence.

With this in mind, let's investigate the case where the iterations were seen to diverge despite choosing an initial guess quite close to the root. To recall, the iterations were setup in the following manner: $$x = \exp(-x)-2x$$.

In [13]:
Niter = 10;
x = np.zeros(Niter);
xg =0.25;

def fx(x):
    return np.exp(-x)-2*x;

xp = np.linspace(-5, 5, 100);
yp = fx(xp);
plt.plot(xp, yp, label='RHS function');
plt.plot(xp, xp, label='$y = x$');
plt.xlim(0, 0.5)
plt.ylim(-0.5,2)

for i in np.arange(1, Niter):
    xn = fx(xg);
    plt.plot(xg, fx(xg), 'ok', markerfacecolor='black', markersize=2);
    x2 = xn; y2 = x2;
    plt.plot(x2, y2, 'ok', markerfacecolor='black', markersize=2 );
    plt.arrow(xg, fx(xg), x2-xg, y2-fx(xg), length_includes_head=True, linewidth=0.7, color='b');
    plt.arrow(x2, y2, x2-x2, fx(x2)-y2, length_includes_head=True, linewidth=0.7, color='r');
    xg = xn;
    

There are a few observables from the plot:

  • We have initialized really close to the root, despite that the root seems to fly away.
  • The negative slope of the curve leads to a spiral nature of iterations.
  • The magnitude of the slope at the root is larger than 1 at the root (verify this yourself). This causes the instability.
  • The figure looks like a spider cobweb!

Using Python inbuilt libraries

We can use one of several functions available in the submodule scipy.optimize for solving nonlinear equations. Let us demonstrate this below:

In [14]:
import scipy.optimize as sco;

Let us solve the nonlinear equation $\exp(x) - 3x^2 =0$ using the

  • fsolve function of python
  • brentq function of python; this is a bracketing method
  • newton function of python; this is the classic Newton-Raphson method. We must supply a Jacobian for this. This is essentially the derivative (analytical) of the function. In case we do not specify the derivative, the secant method is employed in place of the Newton-Raphson method

In all the cases the functions are passed to the solver by means of a function handle. This is illustrated below:

In [15]:
def fx(x):
    return np.exp(x)-3*x**2; 

def fpx(x):
    return np.exp(x)-6*x; 

sol = sco.fsolve(fx, -2);
print(sol)

sol2 = sco.brentq(fx, -2, 0);
print(sol2)

sol3 = sco.newton(fx, -2, fpx); # Jacobian
print(sol3)
[-0.45896227]
-0.45896226753694847
-0.45896226753694863
In [ ]: