184 lines
4.7 KiB
Python
184 lines
4.7 KiB
Python
|
#!/usr/bin/env python3
|
||
|
|
||
|
import numpy as np
|
||
|
from itertools import count
|
||
|
|
||
|
|
||
|
def example_array():
|
||
|
return np.array([[ 3, 2, 1],
|
||
|
[ 2, 3, 2],
|
||
|
[ 1, 2, 3]], dtype=np.float64)
|
||
|
|
||
|
def inverse_power_method(A, sigma, eps, max_iter=None):
|
||
|
"""Determine an eigenvector of matrix A with an eigenvalue closest to sigma.
|
||
|
|
||
|
A -- an N by N square matrix
|
||
|
sigma -- parameter to select eigenvalue
|
||
|
eps -- accuracy
|
||
|
"""
|
||
|
A = np.asarray(A)
|
||
|
N = len(A)
|
||
|
B = np.linalg.inv( A - sigma*np.identity(N))
|
||
|
b_0 = np.ones(N)
|
||
|
|
||
|
|
||
|
for i in count():
|
||
|
b_1 = B @ b_0
|
||
|
|
||
|
b_1 = b_1 / np.linalg.norm(b_1)
|
||
|
|
||
|
error = np.sqrt(np.sum( (np.abs(b_0) - np.abs(b_1))**2 ))
|
||
|
|
||
|
if error < eps:
|
||
|
return b_1, i
|
||
|
|
||
|
# Running out of iterations
|
||
|
if max_iter is not None and max_iter >= i:
|
||
|
raise RuntimeError("Not converging in {} steps".format(i))
|
||
|
|
||
|
# Setup for next loop
|
||
|
b_0 = b_1
|
||
|
|
||
|
|
||
|
def test_inverse_power_method():
|
||
|
A = example_array()
|
||
|
sigma = 1
|
||
|
eps = 1e-4
|
||
|
max_iter = 1e5
|
||
|
eigvec, n = inverse_power_method(A, sigma, eps, None)
|
||
|
|
||
|
_, npeigvecs = np.linalg.eig(A)
|
||
|
|
||
|
rtol = 1e-2
|
||
|
found = False
|
||
|
for npeigvec in npeigvecs.T:
|
||
|
if np.allclose(npeigvec, eigvec, rtol):
|
||
|
found = True
|
||
|
|
||
|
assert found == True
|
||
|
|
||
|
|
||
|
def tridiagonalize(A):
|
||
|
""" Compute the Tridiagonal of the matrix A. """
|
||
|
return householder_tridiagonalize(A)
|
||
|
|
||
|
def householder_tridiagonalize(A):
|
||
|
""" Compute the Tridiagonal of the matrix A using the Householder Method. """
|
||
|
|
||
|
A = np.asarray(A)
|
||
|
|
||
|
N = len(A)
|
||
|
|
||
|
for k in range(N-1):
|
||
|
q = np.sqrt(np.sum( A[k+1:,k]**2 ))
|
||
|
alpha = -q * np.sign(A[k+1,k])
|
||
|
r = np.sqrt( (alpha**2 - A[k+1,k]*alpha)/2 )
|
||
|
|
||
|
v = np.zeros(N)
|
||
|
v[k+1] = (A[k+1, k] - alpha)
|
||
|
v[k+2:] = A[k+2:, k]
|
||
|
v[k + 1:] /= 2 * r
|
||
|
|
||
|
|
||
|
P = np.identity(N) - 2 * np.outer(v,v)
|
||
|
|
||
|
A = P @ A @ P
|
||
|
|
||
|
return A
|
||
|
|
||
|
|
||
|
|
||
|
def test_tridiagonalize():
|
||
|
""" Compare the householder tridiagonalize function with Numpy's Hessenberg. """
|
||
|
from scipy.linalg import hessenberg
|
||
|
|
||
|
A = example_array()
|
||
|
T = tridiagonalize(A)
|
||
|
H = hessenberg(A)
|
||
|
|
||
|
rtol = 1e-3
|
||
|
print(T)
|
||
|
print(H)
|
||
|
print(np.isclose(H, T, rtol))
|
||
|
|
||
|
print("They are almost the same except for a minus sign at (2,3) and (3,2)")
|
||
|
|
||
|
|
||
|
def QREig(T, eps, max_iter=None):
|
||
|
"""Compute Eigenvalues using QR decomposition."""
|
||
|
for i in count():
|
||
|
Q, R = np.linalg.qr(T)
|
||
|
|
||
|
T = R @ Q
|
||
|
|
||
|
# We are done if the sub-diagonal is smaller than eps
|
||
|
d_1 = np.diag(T, -1)
|
||
|
if np.linalg.norm(d_1) < eps:
|
||
|
return np.diagonal(T), i
|
||
|
|
||
|
# Running out of iterations
|
||
|
if max_iter is not None and max_iter >= i:
|
||
|
raise RuntimeError("Not converging in {} steps".format(i))
|
||
|
|
||
|
def test_QREig():
|
||
|
A = example_array()
|
||
|
|
||
|
T = tridiagonalize(A)
|
||
|
eps = 1e-6
|
||
|
|
||
|
eigvals, _ = QREig(T, eps)
|
||
|
npeigvals, npeigvecs = np.linalg.eig(T)
|
||
|
|
||
|
assert np.allclose(eigvals, npeigvals, eps), "The computated eigenvalues do not match Numpy's eigenvalues"
|
||
|
|
||
|
|
||
|
def eig(A, QReps, PMeps, sigma=1e-2):
|
||
|
"""Determine eigenvectors with a combination of QR decomposition and inverse Power Method."""
|
||
|
|
||
|
T = tridiagonalize(A)
|
||
|
eigvals, _ = QREig(T, QReps)
|
||
|
eigvecs = np.zeros((len(eigvals),len(A)), dtype=np.float64)
|
||
|
|
||
|
sigma = (2*np.random.rand() -1 )*sigma
|
||
|
for i, eigval in enumerate(eigvals):
|
||
|
eigvecs[i], _ = inverse_power_method(T, eigval + sigma, PMeps)
|
||
|
|
||
|
return eigvals, eigvecs
|
||
|
|
||
|
|
||
|
def test_eig(debug=True):
|
||
|
A = example_array()
|
||
|
|
||
|
eps = 1e-5
|
||
|
rtol = 1e-1
|
||
|
eigvals, eigvecs = eig(A, eps, eps)
|
||
|
|
||
|
if debug:
|
||
|
for i, eigval in enumerate(eigvals):
|
||
|
print("Eigval {}: ".format(eigval), eigvecs[i])
|
||
|
|
||
|
npeigvals, npeigvecs = np.linalg.eig(A)
|
||
|
if debug:
|
||
|
for i, npeigval in enumerate(npeigvals):
|
||
|
print("Numpy Eigval {}: ".format(npeigval), npeigvecs[i])
|
||
|
|
||
|
assert np.allclose(eigvals, npeigvals, eps), "The computated eigenvalues do not match Numpy's eigenvalues"
|
||
|
|
||
|
#assert np.allclose(eigvecs, npeigvecs, rtol), "The computated eigenvectors do not match Numpy's eigenvectors"
|
||
|
|
||
|
found = np.zeros(len(eigvecs))
|
||
|
for i, eigvec in enumerate(eigvecs):
|
||
|
for npeigvec in npeigvecs:
|
||
|
if np.allclose(eigvec, npeigvec, rtol):
|
||
|
found[i] = 1
|
||
|
break
|
||
|
|
||
|
print("Currently only the first eigenvector is the same from Numpy and my implementation. The others have a minus sign.")
|
||
|
print("Comparable eigenvectors: ", found)
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
#test_inverse_power_method()
|
||
|
#test_tridiagonalize()
|
||
|
#test_QREig()
|
||
|
test_eig()
|