""" Solvers for eigenvalue / eigenvector problems """ from typing import Callable import numpy from numpy.typing import NDArray, ArrayLike from numpy.linalg import norm from scipy import sparse # type: ignore import scipy.sparse.linalg as spalg # type: ignore def power_iteration( operator: sparse.spmatrix, guess_vector: NDArray[numpy.complex128] | None = None, iterations: int = 20, ) -> tuple[complex, NDArray[numpy.complex128]]: """ Use power iteration to estimate the dominant eigenvector of a matrix. 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) """ if guess_vector is None: v = numpy.random.rand(operator.shape[0]) + 1j * numpy.random.rand(operator.shape[0]) else: v = guess_vector for _ in range(iterations): v = operator @ v v /= numpy.abs(v).sum() # faster than true norm v /= norm(v) lm_eigval = v.conj() @ (operator @ v) return lm_eigval, v def rayleigh_quotient_iteration( operator: sparse.spmatrix | spalg.LinearOperator, guess_vector: NDArray[numpy.complex128], iterations: int = 40, tolerance: float = 1e-13, solver: Callable[..., NDArray[numpy.complex128]] | None = None, ) -> tuple[complex, NDArray[numpy.complex128]]: """ Use Rayleigh quotient iteration to refine an eigenvector guess. Args: operator: Matrix to analyze. guess_vector: Eigenvector 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) """ try: (operator - sparse.eye(operator.shape[0])) def shift(eigval: float) -> sparse: return eigval * sparse.eye(operator.shape[0]) if solver is None: solver = spalg.spsolve except TypeError: def shift(eigval: float) -> spalg.LinearOperator: return spalg.LinearOperator( shape=operator.shape, dtype=operator.dtype, matvec=lambda v: eigval * v, ) if solver is None: def solver(A: spalg.LinearOperator, b: ArrayLike) -> NDArray[numpy.complex128]: return spalg.bicgstab(A, b)[0] assert solver is not None v = numpy.squeeze(guess_vector) v /= norm(v) for _ in range(iterations): eigval = v.conj() @ (operator @ v) if norm(operator @ v - eigval * v) < tolerance: break shifted_operator = operator - shift(eigval) v = solver(shifted_operator, v) v /= norm(v) return eigval, v def signed_eigensolve( operator: sparse.spmatrix | spalg.LinearOperator, how_many: int, negative: bool = False, ) -> tuple[NDArray[numpy.complex128], NDArray[numpy.complex128]]: """ Find the largest-magnitude positive-only (or negative-only) eigenvalues and eigenvectors of the provided matrix. 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 """ # 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 range `0 >= neg_eigval + abs(lm_eigval) > abs(lm_eigval)` ''' shift = numpy.abs(lm_eigval) if negative: 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) shifted_eigenvalues, eigenvectors = spalg.eigs(shifted_operator, which='LM', k=how_many, ncv=50) eigenvalues = shifted_eigenvalues - shift k = eigenvalues.argsort() return eigenvalues[k], eigenvectors[:, k]