5. Iterative Computation#
Many algorithm involve iterative computation until a solution is found with a desired accuracy.
Also many mathematical models are formulated as a mapping from the current state to the next state, which gives discrete-time dynamics.
References:
Python Tutorial chapter 4: Control Flow Tools
Wikipedia: Newton’s method, Logistic map
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
Newton’s method#
For a linear equation \(A x = b\), or \(A x - b = 0\), the solution is given using the inverse matrix as $\(x = A^{-1} b. \)$
For a general nonlinear equation \(f(x) = 0\), the solution may require iterative approximation.
A typical way is by the Newton’s method:
$\(x_{k+1} = x_k - \frac{f(x_k)}{f'(x_k)} \)\(
starting from an initial guess \)x_0$.
Example#
Let us define a plynomial function and its derivative.
def poly(x, a, deriv=True):
"""Polynomial a[0] + a[1]*x + ... + a[n]*x**n
and its derivative a[1] + 2*a[2]*x + ... + n*a[n]*x**(n-1)"""
n = len(a) # order+1
xp = np.array([x**i for i in range(n)]) # 1, x,.. x**(n-1)
y = np.dot(a, xp)
if deriv==False:
return y
xq = np.array([i*x**(i-1) for i in range(1,n)]) # 1, 2*x,.. n*x**(n-1))
dy = np.dot(a[1:], xq)
return y, dy
poly(1, [3, 2, 1])
(6, 4)
poly(1, [3, 2, 1], deriv=False)
6
Here is an example polynomial with three zero-crossing points.
# f(x) = (x-3)(x-1)(x+2) = 6 - 5x -2x^2 + x^3
a = np.array([6, -5, -2, 1])
#a = np.random.randn(4) # random coefficients
print(a)
x = np.linspace(-3, 5, 50)
y, dy = poly(x, a)
plt.plot(x, y, x, dy)
plt.grid('on'); plt.xlabel('x'); plt.ylabel('y');
plt.legend(('f(x)','f\'(x)'));
[ 6 -5 -2 1]
![_images/31f58fd3a7e0a141fadadefd600dbfc381b0481eb7b93d257330fabbe0e5a95d.png](_images/31f58fd3a7e0a141fadadefd600dbfc381b0481eb7b93d257330fabbe0e5a95d.png)
Here’s a simple implementation of Newton’s method with visualization.
def newton(f, x0, a, target=1e-6, maxstep=20):
"""Newton's method.
f: return f and df/dx
x: initial guess
a: parameter for f(x,a)
target: accuracy target"""
x = np.zeros(maxstep+1)
y = np.zeros(maxstep)
x[0] = x0
for i in range(maxstep):
y[i], dy = f(x[i], a)
if abs(y[i]) < target:
break # converged!
x[i+1] = x[i] - y[i]/dy # new x
else:
print('did not coverge in', maxstep, 'steps.')
return x[:i+1], y[:i+1]
Let us see how it works.
newton(poly, 5, a)
(array([5. , 3.88 , 3.27527854, 3.04063338, 3.00110572,
3.00000085, 3. ]),
array([5.60000000e+01, 1.49022720e+01, 3.30409339e+00, 4.17958407e-01,
1.10657543e-02, 8.54778237e-06, 5.11590770e-12]))
newton(poly, 0, a)
(array([0. , 1.2 , 0.98978102, 0.99998301, 1. ]),
array([ 6.00000000e+00, -1.15200000e+00, 6.14172290e-02, 1.01951561e-04,
2.88712054e-10]))
Here is a graphical representation.
def zigsawplot(x, y):
"""zigsaw lines of updates
(x0,0),(x0,y0),(x1,0), (x1,y1),...(xn,0),(xn,yn)"""
x = np.repeat(x, 2) # duplicate items
y = np.c_[np.zeros_like(y),y].ravel() # insert zeros
plt.plot(x, y)
plt.plot(x, y)
xt, yt = newton(poly, 5, a)
zigsawplot(xt, yt)
plt.grid('on'); plt.xlabel('x'); plt.ylabel('y');
![_images/92c35122c54c183191bd184cf70d82a44da85b7893fd2bc840ba8f3cd3e49a3d.png](_images/92c35122c54c183191bd184cf70d82a44da85b7893fd2bc840ba8f3cd3e49a3d.png)
Try starting from other initial guesses
plt.plot(x, y)
xt, yt = newton(poly, -3, a)
zigsawplot(xt, yt)
plt.grid('on'); plt.xlabel('x'); plt.ylabel('y');
![_images/7b385dbd8ef66d00c7e156fe3fe5414b4bc583a9aca07756ed661d754866d6a8.png](_images/7b385dbd8ef66d00c7e156fe3fe5414b4bc583a9aca07756ed661d754866d6a8.png)
plt.plot(x, y)
xt, yt = newton(poly, 1.9, a)
zigsawplot(xt, yt)
plt.grid('on'); plt.xlabel('x'); plt.ylabel('y');
![_images/bfb6dbe5d712725234aedc40e630ec0cf4da6d4f1efec24032219b07b2923553.png](_images/bfb6dbe5d712725234aedc40e630ec0cf4da6d4f1efec24032219b07b2923553.png)
plt.plot(x, y)
xt, yt = newton(poly, 2.1, a)
zigsawplot(xt, yt)
plt.grid('on'); plt.xlabel('x'); plt.ylabel('y');
![_images/0a2082644f2d49a804f88aa463e907ca65976d88dc0438ab709be735d645431f.png](_images/0a2082644f2d49a804f88aa463e907ca65976d88dc0438ab709be735d645431f.png)
Quasi-Newton Method#
The derivative \(f'(x)\) may not be available or hard to compute. In that case, we can use the slope between two points to approximate the derivative. $\(f'(x) \simeq \frac{f(x_k)-f(x_{k-1})}{x_k-x_{k-1}} \)$
Quasi-Neuton method uses pairs of points to find the solution: $\(x_{k+1} = x_k - f(x_k)\frac{x_k-x_{k-1}}{f(x_k)-f(x_{k-1})} \)\( starting from an initial guess \)x_0$.
def qnewton(f, x0, x1, *args, target=1e-6, maxstep=20):
"""Quasi-Newton's method.
f: return f
x: initial guess
x0, x1: initial guess at two points
*args: parameter for f(x,*args)
target: accuracy target"""
x = np.zeros(maxstep+2)
y = np.zeros(maxstep+1)
x[0], x[1] = x0, x1
y[0] = f(x[0], *args)
for i in range(1, maxstep+1):
y[i] = f(x[i], *args)
dy = (y[i] - y[i-1])/(x[i] - x[i-1]) # approximate derivative
if abs(y[i]) < target:
break # converged!
x[i+1] = x[i] - y[i]/dy # new x
else:
print('did not coverge in', maxstep, 'steps.')
return x[:i+1], y[:i+1]
qnewton(poly, 5, 4, a, False)
(array([5. , 4. , 3.52631579, 3.19955654, 3.05234137,
3.00641035, 3.00022742, 3.00000102, 3. ]),
array([5.60000000e+01, 1.80000000e+01, 7.34800991e+00, 2.28227200e+00,
5.42734403e-01, 6.43913708e-02, 2.27452245e-03, 1.01671083e-05,
1.61831082e-09]))
def qnplot(x, y):
"""lines for quasi-Newton updates
(x0,0),(x0,y0),(x2,0), (x1,0),(x1,y1),(x3,0),...(xn,0),(xn,yn)"""
for i in range(len(x)-2):
plt.plot([x[i],x[i],x[i+2]], [0,y[i],0])
plt.plot(x, y)
xt, yt = qnewton(poly, 5, 4, a, False)
qnplot(xt, yt)
plt.grid('on'); plt.xlabel('x'); plt.ylabel('y');
![_images/0767ff9f516c6ebb4021100e59e1c86b9dbd4445da80acac0986ee6dbebe8c6d.png](_images/0767ff9f516c6ebb4021100e59e1c86b9dbd4445da80acac0986ee6dbebe8c6d.png)
plt.plot(x, y)
xt, yt = qnewton(poly, 0, 2, a, False)
qnplot(xt, yt)
plt.grid('on'); plt.xlabel('x'); plt.ylabel('y');
![_images/b47a654f5833d9d50732ebd56914560e81d9c9507f24d1ac6f9b56ada851dc91.png](_images/b47a654f5833d9d50732ebd56914560e81d9c9507f24d1ac6f9b56ada851dc91.png)
plt.plot(x, y)
xt, yt = qnewton(poly, -4, -3, a, False)
qnplot(xt, yt)
plt.grid('on'); plt.xlabel('x'); plt.ylabel('y');
![_images/a9506182e7bb1f718db204b3082528a1d554b39208bb48de373a17a637503e00.png](_images/a9506182e7bb1f718db204b3082528a1d554b39208bb48de373a17a637503e00.png)
Define your own function and see how Newton’s or Quasi-Newton method works.
Discrete-time Dynamics#
We have already seen the iterative dynamics by multiplication of a matrix, which can cause expansion, shrinkage, and rotation.
With nonlinear mapping, more varieties of behaviors including chaos can be observed.
1D Dynamics#
The simplest case is the logistic map. $\( x_{k+1} = a x_k(1 - x_k) \)$
def logistic(x, a):
"""logistic map: f(x) = a*x*(1-x)"""
return a*x*(1 - x)
x = np.linspace(0, 1, 50)
# plot with different levels of a
A = np.arange(5)
leg = []
for a in A:
y = logistic(x, a)
plt.plot(x, y)
leg.append('a = {}'.format(a))
plt.legend(leg)
plt.plot([0,1], [0,1]) # x=f(x) line
plt.xlabel('x'); plt.ylabel('f(x)')
plt.axis('square');
![_images/372e54905a7f32481ebe2d6d463047b845dfd1f14977256d996accd3b837c68e.png](_images/372e54905a7f32481ebe2d6d463047b845dfd1f14977256d996accd3b837c68e.png)
def iterate(f, x0, a, steps=100):
"""x0: initial value
a: parameter to f(x,a)"""
x = np.zeros(steps+1)
x[0] = x0
for k in range(steps):
x[k+1] = f(x[k], a)
return x
Try iteration with different parameter \(a\).
a = 1
xt = iterate(logistic, 0.1, a, 200)
plt.plot(xt);
![_images/8e421a8b6b3b914a8e9e221d208e1950c8c1f5d0918754f44e7dab795c4cd982.png](_images/8e421a8b6b3b914a8e9e221d208e1950c8c1f5d0918754f44e7dab795c4cd982.png)
a = 3.5
xt = iterate(logistic, 0.1, a, 200)
plt.plot(xt);
![_images/1086a7e362c04b8d325e46566920ca18fdc56331aef7a288a2b43b2e72722f6e.png](_images/1086a7e362c04b8d325e46566920ca18fdc56331aef7a288a2b43b2e72722f6e.png)
Trajectory in \(x_k\)-\(x_{k+1}\) plane, called “cobsplot”.
def cobsplot(x):
"""cobsplot of trajectory x"""
plt.plot([0,1], [0,1]) # x=f(x) line
x2 = np.repeat(x, 2) # duplicate items
plt.plot(x2[:-1], x2[1:], lw=0.5) # (x0,x1), (x1,x1), (x1,x2),...
plt.xlabel('$x_k$'); plt.ylabel('$x_{k+1}$');
plt.axis('square');
y = logistic(x, a)
plt.plot(x, y) # plot the map
cobsplot(xt) # plot the trajectory
![_images/859d81a9068715e47f5cef76b5accd232e0db0d7fe8b48d05d7e8c1ce5945c72.png](_images/859d81a9068715e47f5cef76b5accd232e0db0d7fe8b48d05d7e8c1ce5945c72.png)
It is known that \(3.57<a<4\) can cause chaotic oscillation.
a = 3.8
y = logistic(x, a)
plt.plot(x, y) # plot the map
xt = iterate(logistic, 0.1, a, 200)
cobsplot(xt) # plot the trajectory
![_images/1ea7a948557249c102260927e2b4c94b6289f46b24b3c1f2e5c48aee0b83ce9c.png](_images/1ea7a948557249c102260927e2b4c94b6289f46b24b3c1f2e5c48aee0b83ce9c.png)
2D Dynamics#
Let us see an example of the Hénon map: $\( x_{k+1} = 1 - a x_k^2 + y_k \)\( \)\( y_{k+1} = b x_k \)$
def henon(xy, ab=[1.4, 0.3]):
"""Henon map: stretch in y and fold to x
x_{k+1} = 1 - a*x_k**2 + y_k
y_{k+1} = b*x_k
xy: state [x, y]
ab: parameters [a, b]
"""
x = 1 - ab[0]*xy[0]**2 + xy[1]
y = ab[1]*xy[0]
return np.array([x, y])
henon([1,1])
array([0.6, 0.3])
def iterate_vec(f, x0, *args, steps=100):
"""f: n-dimensional map
x0: initial vector
*args: optional parameter to f(x,*args) """
n = len(x0) # state dimension
x = np.zeros((steps+1, n))
x[0] = x0
for k in range(steps):
x[k+1] = f(x[k], *args)
return x
x = iterate_vec(henon, [1, 0])
plt.plot(x);
![_images/543d6f0c3f373326234c3868f3b3c59e41707e512b5e5e6c992ca381de247745.png](_images/543d6f0c3f373326234c3868f3b3c59e41707e512b5e5e6c992ca381de247745.png)
x = iterate_vec(henon, [1, 0], [1.4, 0.3], steps=5000)
plt.plot(x[:,0], x[:,1], '.', markersize=0.5)
plt.xlabel('x'); plt.ylabel('y'); plt.axis('equal');
![_images/294e6cf5334f452a2e31d91a74750a74db2f58a6357736726ba5b6e691140085.png](_images/294e6cf5334f452a2e31d91a74750a74db2f58a6357736726ba5b6e691140085.png)
x
array([[ 1. , 0. ],
[-0.4 , 0.3 ],
[ 1.076 , -0.12 ],
...,
[ 1.18897557, 0.04904292],
[-0.93008516, 0.35669267],
[ 0.14561091, -0.27902555]])
Here’s another example: Gumowski-Mira map, which originates from a model of accelerator beams: $\( x_{n+1} = y_n + \alpha y_n (1-\sigma y_n^2) + g(x_n) \)\( \)\( y_{n+1} = -x_n + g(x_{n+1}) \)\( \)\( g(x) = \mu x + \frac{2(1-\mu)x^2}{1+x^2} \)$
def gumowski_mira(xy, asm=[0.009, 0.05, -0.801]):
"""Gumowski-Mira map:
x_{k+1} = y_n + \alpha y_n (1-\sigma y_n^2) + g(x_n
y_{k+1} = -x_n + g(x_{n+1})
g(x) = \mu x + \frac{2(1-\mu)x^2}{1+x^2}
xy: state [x, y]
asm: parameters [a, sigma, mu]
"""
x, y = np.array(xy, dtype=float) # unpack array
alpha, sigma, mu = np.array(asm, dtype=float)
# local function
def g(x):
return mu*x + 2*(1-mu)*x**2/(1+x**2)
x1 = y + alpha*y*(1 - sigma*y**2) + g(x)
y1 = -x + g(x1)
return np.array([x1, y1])
<>:2: SyntaxWarning: invalid escape sequence '\s'
<>:2: SyntaxWarning: invalid escape sequence '\s'
/var/folders/0z/3cttnw852b959kp4wjh_z_wc0000gn/T/ipykernel_51667/728646862.py:2: SyntaxWarning: invalid escape sequence '\s'
"""Gumowski-Mira map:
x = iterate_vec(gumowski_mira, [1, 1], steps=10000)
plt.plot(x[:,0], x[:,1], '.', markersize=0.5);
![_images/e13d8fbc7814b686e9c940437a1a9566cf75d5766c0c3cab8a1060a76e6169ce.png](_images/e13d8fbc7814b686e9c940437a1a9566cf75d5766c0c3cab8a1060a76e6169ce.png)
Recursive call#
In Python and other modern languages, a function and call itself from inside. This can allow compact coding for complex functions.
def factorial(n):
"""factorial by recursice call"""
if n == 0:
return 1
else:
return factorial(n-1)*n
factorial(4)
24
Drawing fractals#
Recursive calls can be used for drawing fractal figures that have multi-scale self-similar structures.
One example is the Cantor set, which is made by iteratively removing the middle part of a segment.
def cantorplot(x0=0, x1=1, a=1/3, e=1e-4):
"""draw a Cantor set
x0, x1: end points
a: fraction to fill
e: minimal resolution"""
u = x1 - x0 # length
if abs(u) < e: # for a very short segment
plt.plot([x0, x1], [0, 0]) # straight line
else:
cantorplot(x0, x0+a*u, a, e) # left 1/3
cantorplot(x1-a*u, x1, a, e) # right 1/3
# Interactive mode to allow zooming up
%matplotlib notebook
plt.figure(figsize=(10,2))
cantorplot(a=0.4)
Here is an example of drawing complex lines like a coastline or a clowd by replacing a segment with zig-zag segments.
def fractaline(x0=[0,0], x1=[1,0], a=[0.1, -0.1], e=1e-2):
"""draw a fractal line
x0, x1: start, end points
a: shifts of via points
e: minimal resolution"""
n = np.size(a) # number of via points
x = np.zeros((n+2,2))
x[0], x[-1] = x0, x1
u = x[-1] - x[0] # connecting vector
v = np.array([-u[1],u[0]]) # orthogonal vector
for i, ai in enumerate(a):
x[i+1] = x0 + u*(i+1)/(n+1) + ai*v # shift of via points
#print(x)
if sum((u**2)) < e**2: # if the segment is very short
plt.plot(x[:,0],x[:,1]) # draw a straight line
else:
for i in range(n+1): # n segments
fractaline(x[i], x[i+1], a, e)
plt.axis('equal');
plt.figure(figsize=(10,4))
fractaline()
plt.figure(figsize=(10,5))
fractaline(a=[0.2])