def test_all_zero(): tol = Tolerance(atol=5, rtol=9999999) assert tol.all_near_zero(0) assert tol.all_near_zero(4.5) assert not tol.all_near_zero(5.5) assert tol.all_near_zero([-4.5, 0, 1, 4.5, 3]) assert not tol.all_near_zero([-4.5, 0, 1, 4.5, 30])
def _perp_eigendecompose( matrix: np.ndarray, tolerance: Tolerance) -> Tuple[np.array, List[np.ndarray]]: """An eigendecomposition that ensures eigenvectors are perpendicular. numpy.linalg.eig doesn't guarantee that eigenvectors from the same eigenspace will be perpendicular. This method uses Gram-Schmidt to recover a perpendicular set. It further checks that all eigenvectors are perpendicular and raises an ArithmeticError otherwise. Args: matrix: The matrix to decompose. tolerance: Thresholds for determining whether eigenvalues are from the same eigenspace and whether eigenvectors are perpendicular. Returns: The eigenvalues and column eigenvectors. The i'th eigenvalue is associated with the i'th column eigenvector. Raises: ArithmeticError: Failed to find perpendicular eigenvectors. """ vals, cols = np.linalg.eig(matrix) vecs = [cols[:, i] for i in range(len(cols))] # Convert list of row arrays to list of column arrays. for i in range(len(vecs)): vecs[i] = np.reshape(vecs[i], (len(vecs[i]), vecs[i].ndim)) # Group by similar eigenvalue. n = len(vecs) groups = _group_similar( list(range(n)), lambda k1, k2: tolerance.all_close(vals[k1], vals[k2])) # Remove overlap between eigenvectors with the same eigenvalue. for g in groups: q, _ = np.linalg.qr(np.hstack([vecs[i] for i in g])) for i in range(len(g)): vecs[g[i]] = q[:, i] # Ensure no eigenvectors overlap. for i in range(len(vecs)): for j in range(i + 1, len(vecs)): if not tolerance.all_near_zero(np.dot(np.conj(vecs[i].T), vecs[j])): raise ArithmeticError('Eigenvectors overlap.') return vals, vecs
def is_diagonal(matrix: np.ndarray, tolerance: Tolerance = Tolerance.DEFAULT) -> bool: """Determines if a matrix is a approximately diagonal. A matrix is diagonal if i!=j implies m[i,j]==0. Args: matrix: The matrix to check. tolerance: The per-matrix-entry tolerance on equality. Returns: Whether the matrix is diagonal within the given tolerance. """ matrix = np.copy(matrix) for i in range(min(matrix.shape)): matrix[i, i] = 0 return tolerance.all_near_zero(matrix)
def _perp_eigendecompose(matrix: np.ndarray, tolerance: Tolerance ) -> Tuple[np.array, List[np.ndarray]]: """An eigendecomposition that ensures eigenvectors are perpendicular. numpy.linalg.eig doesn't guarantee that eigenvectors from the same eigenspace will be perpendicular. This method uses Gram-Schmidt to recover a perpendicular set. It further checks that all eigenvectors are perpendicular and raises an ArithmeticError otherwise. Args: matrix: The matrix to decompose. tolerance: Thresholds for determining whether eigenvalues are from the same eigenspace and whether eigenvectors are perpendicular. Returns: The eigenvalues and column eigenvectors. The i'th eigenvalue is associated with the i'th column eigenvector. Raises: ArithmeticError: Failed to find perpendicular eigenvectors. """ vals, cols = np.linalg.eig(np.mat(matrix)) vecs = [cols[:, i] for i in range(len(cols))] # Group by similar eigenvalue. n = len(vecs) groups = _group_similar( list(range(n)), lambda k1, k2: tolerance.all_close(vals[k1], vals[k2])) # Remove overlap between eigenvectors with the same eigenvalue. for g in groups: q, _ = np.linalg.qr(np.concatenate([vecs[i] for i in g], axis=1)) for i in range(len(g)): vecs[g[i]] = q[:, i] # Ensure no eigenvectors overlap. for i in range(len(vecs)): for j in range(i + 1, len(vecs)): if not tolerance.all_near_zero(np.dot(np.conj(vecs[i].T), vecs[j])): raise ArithmeticError('Eigenvectors overlap.') return vals, vecs
def bidiagonalize_real_matrix_pair_with_symmetric_products( mat1: np.ndarray, mat2: np.ndarray, tolerance: Tolerance = Tolerance.DEFAULT ) -> Tuple[np.ndarray, np.ndarray]: """Finds orthogonal matrices that diagonalize both mat1 and mat2. Requires mat1 and mat2 to be real. Requires mat1.T @ mat2 to be symmetric. Requires mat1 @ mat2.T to be symmetric. Args: mat1: One of the real matrices. mat2: The other real matrix. tolerance: Numeric error thresholds. Returns: A tuple (L, R) of two orthogonal matrices, such that both L @ mat1 @ R and L @ mat2 @ R are diagonal matrices. Raises: ValueError: Matrices don't meet preconditions (e.g. not real). ArithmeticError: Failed to meet specified tolerance. """ if np.any(np.imag(mat1) != 0): raise ValueError('mat1 must be real.') if np.any(np.imag(mat2) != 0): raise ValueError('mat2 must be real.') if not predicates.is_hermitian(mat1.dot(mat2.T), tolerance): raise ValueError('mat1 @ mat2.T must be symmetric.') if not predicates.is_hermitian(mat1.T.dot(mat2), tolerance): raise ValueError('mat1.T @ mat2 must be symmetric.') # Use SVD to bi-diagonalize the first matrix. base_left, base_diag, base_right = _svd_handling_empty(np.real(mat1)) base_diag = np.diag(base_diag) # Determine where we switch between diagonalization-fixup strategies. dim = base_diag.shape[0] rank = dim while rank > 0 and tolerance.all_near_zero(base_diag[rank - 1, rank - 1]): rank -= 1 base_diag = base_diag[:rank, :rank] # Try diagonalizing the second matrix with the same factors as the first. semi_corrected = base_left.T.dot(np.real(mat2)).dot(base_right.T) # Fix up the part of the second matrix's diagonalization that's matched # against non-zero diagonal entries in the first matrix's diagonalization # by performing simultaneous diagonalization. overlap = semi_corrected[:rank, :rank] overlap_adjust = diagonalize_real_symmetric_and_sorted_diagonal_matrices( overlap, base_diag, tolerance) # Fix up the part of the second matrix's diagonalization that's matched # against zeros in the first matrix's diagonalization by performing an SVD. extra = semi_corrected[rank:, rank:] extra_left_adjust, _, extra_right_adjust = _svd_handling_empty(extra) # Merge the fixup factors into the initial diagonalization. left_adjust = combinators.block_diag(overlap_adjust, extra_left_adjust) right_adjust = combinators.block_diag(overlap_adjust.T, extra_right_adjust) left = left_adjust.T.dot(base_left.T) right = base_right.T.dot(right_adjust.T) # Check acceptability vs tolerances. if any(not predicates.is_diagonal(left.dot(mat).dot(right), tolerance) for mat in [mat1, mat2]): raise ArithmeticError('Failed to diagonalize to specified tolerance.') return left, right