2017-09-24 22:28:08 -07:00
|
|
|
"""
|
|
|
|
Solvers for eigenvalue / eigenvector problems
|
|
|
|
"""
|
|
|
|
from typing import Tuple, List
|
|
|
|
import numpy
|
|
|
|
from numpy.linalg import norm
|
|
|
|
from scipy import sparse
|
|
|
|
import scipy.sparse.linalg as spalg
|
|
|
|
|
|
|
|
|
|
|
|
def power_iteration(operator: sparse.spmatrix,
|
|
|
|
guess_vector: numpy.ndarray = None,
|
|
|
|
iterations: int = 20,
|
|
|
|
) -> Tuple[complex, numpy.ndarray]:
|
|
|
|
"""
|
|
|
|
Use power iteration to estimate the dominant eigenvector of a matrix.
|
|
|
|
|
2019-11-24 23:47:31 -08:00
|
|
|
Args:
|
|
|
|
operator: Matrix to analyze.
|
|
|
|
guess_vector: Starting point for the eigenvector. Default is a randomly chosen vector.
|
|
|
|
iterations: Number of iterations to perform. Default 20.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
(Largest-magnitude eigenvalue, Corresponding eigenvector estimate)
|
2017-09-24 22:28:08 -07:00
|
|
|
"""
|
|
|
|
if numpy.any(numpy.equal(guess_vector, None)):
|
|
|
|
v = numpy.random.rand(operator.shape[0])
|
|
|
|
else:
|
|
|
|
v = guess_vector
|
|
|
|
|
|
|
|
for _ in range(iterations):
|
|
|
|
v = operator @ v
|
|
|
|
v /= norm(v)
|
|
|
|
|
2017-12-09 18:21:37 -08:00
|
|
|
lm_eigval = v.conj() @ (operator @ v)
|
2017-09-24 22:28:08 -07:00
|
|
|
return lm_eigval, v
|
|
|
|
|
|
|
|
|
2017-12-17 20:51:34 -08:00
|
|
|
def rayleigh_quotient_iteration(operator: sparse.spmatrix or spalg.LinearOperator,
|
2019-11-24 22:50:03 -08:00
|
|
|
guess_vectors: numpy.ndarray,
|
2017-09-24 22:28:08 -07:00
|
|
|
iterations: int = 40,
|
|
|
|
tolerance: float = 1e-13,
|
2017-12-17 20:51:34 -08:00
|
|
|
solver=None,
|
2017-09-24 22:28:08 -07:00
|
|
|
) -> Tuple[complex, numpy.ndarray]:
|
|
|
|
"""
|
|
|
|
Use Rayleigh quotient iteration to refine an eigenvector guess.
|
|
|
|
|
2019-11-24 22:50:03 -08:00
|
|
|
TODO:
|
|
|
|
Need to test this for more than one guess_vectors.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
operator: Matrix to analyze.
|
|
|
|
guess_vectors: Eigenvectors to refine.
|
|
|
|
iterations: Maximum number of iterations to perform. Default 40.
|
|
|
|
tolerance: Stop iteration if `(A - I*eigenvalue) @ v < num_vectors * tolerance`,
|
|
|
|
Default 1e-13.
|
|
|
|
solver: Solver function of the form `x = solver(A, b)`.
|
|
|
|
By default, use scipy.sparse.spsolve for sparse matrices and
|
|
|
|
scipy.sparse.bicgstab for general LinearOperator instances.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
(eigenvalues, eigenvectors)
|
2017-09-24 22:28:08 -07:00
|
|
|
"""
|
2017-12-17 20:51:34 -08:00
|
|
|
try:
|
2018-01-06 13:51:42 -08:00
|
|
|
_test = operator - sparse.eye(operator.shape[0])
|
2017-12-17 20:51:34 -08:00
|
|
|
shift = lambda eigval: eigval * sparse.eye(operator.shape[0])
|
|
|
|
if solver is None:
|
|
|
|
solver = spalg.spsolve
|
|
|
|
except TypeError:
|
|
|
|
shift = lambda eigval: spalg.LinearOperator(shape=operator.shape,
|
|
|
|
dtype=operator.dtype,
|
|
|
|
matvec=lambda v: eigval * v)
|
|
|
|
if solver is None:
|
|
|
|
solver = lambda A, b: spalg.bicgstab(A, b)[0]
|
|
|
|
|
2019-11-24 22:50:03 -08:00
|
|
|
v = numpy.atleast_2d(guess_vectors)
|
2017-12-17 20:51:34 -08:00
|
|
|
v /= norm(v)
|
2017-09-24 22:28:08 -07:00
|
|
|
for _ in range(iterations):
|
2017-12-17 20:51:34 -08:00
|
|
|
eigval = v.conj() @ (operator @ v)
|
2019-11-24 22:50:03 -08:00
|
|
|
if norm(operator @ v - eigval * v) < v.shape[1] * tolerance:
|
2017-09-24 22:28:08 -07:00
|
|
|
break
|
|
|
|
|
2017-12-17 20:51:34 -08:00
|
|
|
shifted_operator = operator - shift(eigval)
|
|
|
|
v = solver(shifted_operator, v)
|
2019-11-24 22:50:03 -08:00
|
|
|
v /= norm(v, axis=0)
|
2017-09-24 22:28:08 -07:00
|
|
|
return eigval, v
|
|
|
|
|
|
|
|
|
2017-12-09 18:21:37 -08:00
|
|
|
def signed_eigensolve(operator: sparse.spmatrix or spalg.LinearOperator,
|
2017-09-24 22:28:08 -07:00
|
|
|
how_many: int,
|
|
|
|
negative: bool = False,
|
|
|
|
) -> Tuple[numpy.ndarray, numpy.ndarray]:
|
|
|
|
"""
|
|
|
|
Find the largest-magnitude positive-only (or negative-only) eigenvalues and
|
|
|
|
eigenvectors of the provided matrix.
|
|
|
|
|
2019-11-24 23:47:31 -08:00
|
|
|
Args:
|
|
|
|
operator: Matrix to analyze.
|
|
|
|
how_many: How many eigenvalues to find.
|
|
|
|
negative: Whether to find negative-only eigenvalues.
|
|
|
|
Default False (positive only).
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
(sorted list of eigenvalues, 2D ndarray of corresponding eigenvectors)
|
|
|
|
`eigenvectors[:, k]` corresponds to the k-th eigenvalue
|
2017-09-24 22:28:08 -07:00
|
|
|
"""
|
|
|
|
# Use power iteration to estimate the dominant eigenvector
|
|
|
|
lm_eigval, _ = power_iteration(operator)
|
|
|
|
|
|
|
|
'''
|
|
|
|
Shift by the absolute value of the largest eigenvalue, then find a few of the
|
|
|
|
largest-magnitude (shifted) eigenvalues. A positive shift ensures that we find the
|
|
|
|
largest _positive_ eigenvalues, since any negative eigenvalues will be shifted to the
|
2019-11-24 22:50:03 -08:00
|
|
|
range `0 >= neg_eigval + abs(lm_eigval) > abs(lm_eigval)`
|
2017-09-24 22:28:08 -07:00
|
|
|
'''
|
2017-12-09 18:21:37 -08:00
|
|
|
shift = numpy.abs(lm_eigval)
|
2017-09-24 22:28:08 -07:00
|
|
|
if negative:
|
2017-12-09 18:21:37 -08:00
|
|
|
shift *= -1
|
|
|
|
|
|
|
|
# Try to combine, use general LinearOperator if we fail
|
|
|
|
try:
|
|
|
|
shifted_operator = operator + shift * sparse.eye(operator.shape[0])
|
|
|
|
except TypeError:
|
|
|
|
shifted_operator = operator + spalg.LinearOperator(shape=operator.shape,
|
|
|
|
matvec=lambda v: shift * v)
|
2017-09-24 22:28:08 -07:00
|
|
|
|
2017-12-09 18:21:37 -08:00
|
|
|
shifted_eigenvalues, eigenvectors = spalg.eigs(shifted_operator, which='LM', k=how_many, ncv=50)
|
|
|
|
eigenvalues = shifted_eigenvalues - shift
|
2017-09-24 22:28:08 -07:00
|
|
|
|
|
|
|
k = eigenvalues.argsort()
|
|
|
|
return eigenvalues[k], eigenvectors[:, k]
|
|
|
|
|