Computing Eigenvalues#
import numpy as np
import scipy.linalg as la
import matplotlib.pyplot as plt
Power Iteration#
Suppose a matrix \(A\) has a dominant eigenvalue \(\lambda_1\). This means that \(\lambda_1\) has multiplicity 1 and \(| \lambda_1 | > | \lambda |\) for all other eignvalues \(\lambda\). Choose a random vector \(\mathbf{x}_0\) and compute the (normalized) power iteration algorithm:
The result (usually) converges to an eigenvector for \(\lambda_1\). Let us implement the algorithm with pauses at each iteration so that we can see the vector \(\mathbf{x}_k\) at each step.
import time
A = np.array([[1,1,0],[1,1,1],[0,1,1]])
x0 = np.array([[1],[0],[0]])
iterations = 5
print('\nIteration k=0\n\nxk =\n\n{}'.format(x0))
xk = x0
for k in range(1,iterations+1):
xk = A@xk
xk = xk/np.max(np.abs(xk))
print('\nIteration k={0}\n\nxk =\n\n{1}'.format(k,xk))
time.sleep(1)
Iteration k=0
xk =
[[1]
[0]
[0]]
Iteration k=1
xk =
[[1.]
[1.]
[0.]]
Iteration k=2
xk =
[[1. ]
[1. ]
[0.5]]
Iteration k=3
xk =
[[0.8]
[1. ]
[0.6]]
Iteration k=4
xk =
[[0.75 ]
[1. ]
[0.66666667]]
Iteration k=5
xk =
[[0.72413793]
[1. ]
[0.68965517]]
Now let us implement the method without pauses and increase the number of iterations.
A = np.array([[1,1,0],[1,1,1],[0,1,1]])
x0 = np.array([[1],[0],[0]])
iterations = 20
xk = x0
for k in range(0,iterations):
xk = A@xk
xk = xk/np.max(np.abs(xk))
print(xk)
[[0.70710681]
[1. ]
[0.70710675]]
Compute the Rayleigh quotient to approximate the corresponding eigenvalue:
xk.T @ A @ xk / (xk.T @ xk)
array([[2.41421356]])
Compare to the function scipy.linalg.eig
.
evals,evecs = la.eig(A)
print(evals)
[-0.41421356+0.j 1. +0.j 2.41421356+0.j]
print(evecs)
[[ 5.00000000e-01 7.07106781e-01 5.00000000e-01]
[-7.07106781e-01 4.91354351e-16 7.07106781e-01]
[ 5.00000000e-01 -7.07106781e-01 5.00000000e-01]]
Indeed, power iteration produces an approximation of the eigenvector for eigenvalue \(\lambda_1 = 1 + \sqrt{2}\).
Inverse Iteration#
Suppose matrix \(A\) is invertible. Then the smallest eigenvalue \(\lambda_n\) (in absolute value) of \(A\) corresponds to the dominant eigenvalue \(1/\lambda_n\) of \(A^{-1}\). Choose a random vector \(\mathbf{x}_0\) and compute the (normalized) inverse iteration algorithm:
The result (usually) converges to an eigenvector for \(A\) for \(\lambda_n\) (which is also an eigenvector for \(A^{-1}\) for \(1/\lambda_n\)). Let us implement the algorithm.
A = np.array([[1,1,0],[1,1,1],[0,1,1]])
x0 = np.array([[1],[0],[0]])
iterations = 20
xk = x0
LU,P = la.lu_factor(A)
for k in range(0,iterations):
xk = la.lu_solve((LU,P),xk)
xk = xk/np.max(np.abs(xk))
print(xk/la.norm(xk))
[[ 0.50000002]
[-0.70710678]
[ 0.49999998]]
This is the eigenvector for \(A\) for \(\lambda = 1 - \sqrt{2}\).
Random matrices#
Let us implement the power iteration method for \(B^TB\) for a random matrix \(B\) of size \(N\). Since \(B^TB\) is symmetric we know the eigenvalues are real. Compute the Rayleigh quotient of the result and compare with the function scipy.linalg.eig
.
N = 100
B = np.random.randn(N,N)
A = B.T@B
x0 = np.random.randn(N,1)
iterations = 150
xk = x0
for k in range(0,iterations):
xk = A@xk
xk = xk/np.max(np.abs(xk))
lambda_1 = xk.T@A@xk/(xk.T@xk)
print("Power method: ", lambda_1[0,0])
evals,evecs = la.eig(A)
print("SciPy: ", np.max(np.abs(evals.real)))
Power method: 375.75262486612826
SciPy: 376.1225114616083
The result is pretty good for 150 iterations!
QR Iteration#
Start with \(A_0 = A\), for each step \(k\), compute the QR decomposition \(A_k = Q_k R_k\), define \(A_{k+1} = R_k Q_k\) and repeat. The matrices \(A_k\) are similar therefore they have the same eigenvalues. The result is an upper (block) triangular matrix with eigenvalues on the diagonal.
A = np.array([[1,1,0],[1,1,1],[0,1,1]])
iterations = 5
print('\nIteration k=0\n\nA =\n\n{}'.format(A))
Ak = A
for k in range(1,iterations+1):
Q,R = la.qr(Ak)
Ak = R@Q
print('\nIteration k={0}\n\nAk =\n\n{1}'.format(k,Ak))
time.sleep(1)
Iteration k=0
A =
[[1 1 0]
[1 1 1]
[0 1 1]]
Iteration k=1
Ak =
[[ 2.00000000e+00 -7.07106781e-01 2.02930727e-17]
[-7.07106781e-01 1.00000000e+00 7.07106781e-01]
[ 0.00000000e+00 7.07106781e-01 0.00000000e+00]]
Iteration k=2
Ak =
[[ 2.33333333e+00 -3.33333333e-01 -3.20655176e-16]
[-3.33333333e-01 1.00000000e+00 3.33333333e-01]
[ 0.00000000e+00 3.33333333e-01 -3.33333333e-01]]
Iteration k=3
Ak =
[[ 2.40000000e+00 -1.41421356e-01 2.07336995e-16]
[-1.41421356e-01 1.00000000e+00 1.41421356e-01]
[ 0.00000000e+00 1.41421356e-01 -4.00000000e-01]]
Iteration k=4
Ak =
[[ 2.41176471e+00 -5.88235294e-02 -2.50505098e-16]
[-5.88235294e-02 1.00000000e+00 5.88235294e-02]
[ 0.00000000e+00 5.88235294e-02 -4.11764706e-01]]
Iteration k=5
Ak =
[[ 2.41379310e+00 -2.43829925e-02 2.24955710e-16]
[-2.43829925e-02 1.00000000e+00 2.43829925e-02]
[ 0.00000000e+00 2.43829925e-02 -4.13793103e-01]]
Let us implement the method without pauses and increase the number of iterations.
A = np.array([[1,1,0],[1,1,1],[0,1,1]])
iterations = 50
Ak = A
for k in range(0,iterations):
Q,R = la.qr(Ak)
Ak = R@Q
print(Ak)
[[ 2.41421356e+00 -3.77213497e-16 -2.29396765e-16]
[-1.45293347e-19 1.00000000e+00 -1.23531192e-16]
[ 0.00000000e+00 1.45293347e-19 -4.14213562e-01]]
We can see the eigenvalues on the diagonal of the matrix \(A_k\).