def diagonalize_real_symmetric_matrix( matrix: np.ndarray, *, rtol: float = 1e-5, atol: float = 1e-8, check_preconditions: bool = True) -> np.ndarray: """Returns an orthogonal matrix that diagonalizes the given matrix. Args: matrix: A real symmetric matrix to diagonalize. rtol: Relative error tolerance. atol: Absolute error tolerance. check_preconditions: If set, verifies that the input matrix is real and symmetric. Returns: An orthogonal matrix P such that P.T @ matrix @ P is diagonal. Raises: ValueError: Matrix isn't real symmetric. """ if check_preconditions and ( np.any(np.imag(matrix) != 0) or not predicates.is_hermitian(matrix, rtol=rtol, atol=atol)): raise ValueError('Input must be real and symmetric.') _, result = np.linalg.eigh(matrix) return result
def diagonalize_real_symmetric_matrix( matrix: np.ndarray, *, rtol: float = 1e-5, atol: float = 1e-8) -> np.ndarray: """Returns an orthogonal matrix that diagonalizes the given matrix. Args: matrix: A real symmetric matrix to diagonalize. rtol: float = 1e-5, atol: float = 1e-8 Returns: An orthogonal matrix P such that P.T @ matrix @ P is diagonal. Raises: ValueError: Matrix isn't real symmetric. """ # TODO: Determine if thresholds should be passed into is_hermitian if np.any(np.imag(matrix) != 0) or not predicates.is_hermitian(matrix): raise ValueError('Input must be real and symmetric.') _, result = np.linalg.eigh(matrix) return result
def diagonalize_real_symmetric_matrix( matrix: np.ndarray, tolerance: Tolerance = Tolerance.DEFAULT) -> np.ndarray: """Returns an orthogonal matrix that diagonalizes the given matrix. Args: matrix: A real symmetric matrix to diagonalize. tolerance: Numeric error thresholds. Returns: An orthogonal matrix P such that P.T @ matrix @ P is diagonal. Raises: ValueError: Matrix isn't real symmetric. ArithmeticError: Failed to meet specified tolerance. """ if np.any(np.imag(matrix) != 0) or not predicates.is_hermitian(matrix): raise ValueError('Input must be real and symmetric.') _, result = np.linalg.eigh(matrix) # Check acceptability vs tolerances. if (not predicates.is_orthogonal(result, tolerance) or not predicates.is_diagonal( result.T.dot(matrix).dot(result), tolerance)): raise ArithmeticError('Failed to diagonalize to specified tolerance.') return result
def diagonalize_real_symmetric_and_sorted_diagonal_matrices( symmetric_matrix: np.ndarray, diagonal_matrix: np.ndarray, tolerance: Tolerance = Tolerance.DEFAULT) -> np.ndarray: """Returns an orthogonal matrix that diagonalizes both given matrices. The given matrices must commute. Guarantees that the sorted diagonal matrix is not permuted by the diagonalization (except for nearly-equal values). Args: symmetric_matrix: A real symmetric matrix. diagonal_matrix: A real diagonal matrix with entries along the diagonal sorted into descending order. tolerance: Numeric error thresholds. Returns: An orthogonal matrix P such that P.T @ symmetric_matrix @ P is diagonal and P.T @ diagonal_matrix @ P = diagonal_matrix (up to tolerance). Raises: ValueError: Matrices don't meet preconditions (e.g. not symmetric). ArithmeticError: Failed to meet specified tolerance. """ # Verify preconditions. if (np.any(np.imag(symmetric_matrix) != 0) or not predicates.is_hermitian(symmetric_matrix)): raise ValueError('symmetric_matrix must be real symmetric.') if (not predicates.is_diagonal(diagonal_matrix) or np.any(np.imag(diagonal_matrix) != 0) or np.any(diagonal_matrix[:-1, :-1] < diagonal_matrix[1:, 1:])): raise ValueError('diagonal_matrix must be real diagonal descending.') if not predicates.commutes(diagonal_matrix, symmetric_matrix): raise ValueError('Given matrices must commute.') def similar_singular(i, j): return tolerance.all_close(diagonal_matrix[i, i], diagonal_matrix[j, j]) # Because the symmetric matrix commutes with the diagonal singulars matrix, # the symmetric matrix should be block-diagonal with a block boundary # wherever the singular values happen change. So we can use the singular # values to extract blocks that can be independently diagonalized. ranges = _contiguous_groups(diagonal_matrix.shape[0], similar_singular) # Build the overall diagonalization by diagonalizing each block. p = np.zeros(symmetric_matrix.shape, dtype=np.float64) for start, end in ranges: block = symmetric_matrix[start:end, start:end] p[start:end, start:end] = diagonalize_real_symmetric_matrix(block) # Check acceptability vs tolerances. if (not predicates.is_diagonal( p.T.dot(symmetric_matrix).dot(p), tolerance) or not tolerance.all_close(diagonal_matrix, p.T.dot(diagonal_matrix).dot(p))): raise ArithmeticError('Failed to diagonalize to specified tolerance.') return p
def diagonalize_real_symmetric_matrix( matrix: np.ndarray, tolerance: Tolerance = Tolerance.DEFAULT ) -> np.ndarray: """Returns an orthogonal matrix that diagonalizes the given matrix. Args: matrix: A real symmetric matrix to diagonalize. tolerance: Numeric error thresholds. Returns: An orthogonal matrix P such that P.T @ matrix @ P is diagonal. Raises: ValueError: Matrix isn't real symmetric. ArithmeticError: Failed to meet specified tolerance. """ if np.any(np.imag(matrix) != 0) or not predicates.is_hermitian(matrix): raise ValueError('Input must be real and symmetric.') _, result = np.linalg.eigh(matrix) # Check acceptability vs tolerances. if (not predicates.is_orthogonal(result, tolerance) or not predicates.is_diagonal(result.T.dot(matrix).dot(result), tolerance)): raise ArithmeticError('Failed to diagonalize to specified tolerance.') return result
def test_is_hermitian_tolerance(): tol = Tolerance(atol=0.5) # Pays attention to specified tolerance. assert predicates.is_hermitian(np.array([[1, 0], [-0.5, 1]]), tol) assert predicates.is_hermitian(np.array([[1, 0.25], [-0.25, 1]]), tol) assert not predicates.is_hermitian(np.array([[1, 0], [-0.6, 1]]), tol) assert not predicates.is_hermitian(np.array([[1, 0.25], [-0.35, 1]]), tol) # Error isn't accumulated across entries. assert predicates.is_hermitian( np.array([[1, 0.5, 0.5], [0, 1, 0], [0, 0, 1]]), tol) assert not predicates.is_hermitian( np.array([[1, 0.5, 0.6], [0, 1, 0], [0, 0, 1]]), tol) assert not predicates.is_hermitian( np.array([[1, 0, 0.6], [0, 1, 0], [0, 0, 1]]), tol)
def diagonalize_real_symmetric_matrix( matrix: np.ndarray, tolerance: Tolerance = Tolerance.DEFAULT ) -> np.ndarray: """Returns an orthogonal matrix that diagonalizes the given matrix. Args: matrix: A real symmetric matrix to diagonalize. tolerance: Numeric error thresholds. Returns: An orthogonal matrix P such that P.T @ matrix @ P is diagonal. Raises: ValueError: Matrix isn't real symmetric. """ if np.any(np.imag(matrix) != 0) or not predicates.is_hermitian(matrix): raise ValueError('Input must be real and symmetric.') _, result = np.linalg.eigh(matrix) return result
def diagonalize_real_symmetric_and_sorted_diagonal_matrices( symmetric_matrix: np.ndarray, diagonal_matrix: np.ndarray, *, rtol: float = 1e-5, atol: float = 1e-8, check_preconditions: bool = True) -> np.ndarray: """Returns an orthogonal matrix that diagonalizes both given matrices. The given matrices must commute. Guarantees that the sorted diagonal matrix is not permuted by the diagonalization (except for nearly-equal values). Args: symmetric_matrix: A real symmetric matrix. diagonal_matrix: A real diagonal matrix with entries along the diagonal sorted into descending order. rtol: Relative numeric error threshold. atol: Absolute numeric error threshold. check_preconditions: If set, verifies that the input matrices commute and are respectively symmetric and diagonal descending. Returns: An orthogonal matrix P such that P.T @ symmetric_matrix @ P is diagonal and P.T @ diagonal_matrix @ P = diagonal_matrix (up to tolerance). Raises: ValueError: Matrices don't meet preconditions (e.g. not symmetric). """ # Verify preconditions. if check_preconditions: if (np.any(np.imag(symmetric_matrix)) or not predicates.is_hermitian( symmetric_matrix, rtol=rtol, atol=atol)): raise ValueError('symmetric_matrix must be real symmetric.') if (not predicates.is_diagonal(diagonal_matrix, atol=atol) or np.any(np.imag(diagonal_matrix)) or np.any(diagonal_matrix[:-1, :-1] < diagonal_matrix[1:, 1:])): raise ValueError( 'diagonal_matrix must be real diagonal descending.') if not predicates.commutes( diagonal_matrix, symmetric_matrix, rtol=rtol, atol=atol): raise ValueError('Given matrices must commute.') def similar_singular(i, j): return np.allclose(diagonal_matrix[i, i], diagonal_matrix[j, j], rtol=rtol) # Because the symmetric matrix commutes with the diagonal singulars matrix, # the symmetric matrix should be block-diagonal with a block boundary # wherever the singular values happen change. So we can use the singular # values to extract blocks that can be independently diagonalized. ranges = _contiguous_groups(diagonal_matrix.shape[0], similar_singular) # Build the overall diagonalization by diagonalizing each block. p = np.zeros(symmetric_matrix.shape, dtype=np.float64) for start, end in ranges: block = symmetric_matrix[start:end, start:end] p[start:end, start:end] = diagonalize_real_symmetric_matrix( block, rtol=rtol, atol=atol, check_preconditions=False) return p
def bidiagonalize_real_matrix_pair_with_symmetric_products( mat1: np.ndarray, mat2: np.ndarray, *, rtol: float = 1e-5, atol: float = 1e-8, check_preconditions: bool = True) -> 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. rtol: Relative numeric error threshold. atol: Absolute numeric error threshold. check_preconditions: If set, verifies that the inputs are real, and that mat1.T @ mat2 and mat1 @ mat2.T are both symmetric. Defaults to set. 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). """ if check_preconditions: 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), rtol=rtol, atol=atol): raise ValueError('mat1 @ mat2.T must be symmetric.') if not predicates.is_hermitian(mat1.T.dot(mat2), rtol=rtol, atol=atol): 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], atol=atol): 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, rtol=rtol, atol=atol, check_preconditions=check_preconditions) # 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) return left, right
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
def test_is_hermitian(): assert predicates.is_hermitian(np.empty((0, 0))) assert not predicates.is_hermitian(np.empty((1, 0))) assert not predicates.is_hermitian(np.empty((0, 1))) assert predicates.is_hermitian(np.array([[1]])) assert predicates.is_hermitian(np.array([[-1]])) assert predicates.is_hermitian(np.array([[5]])) assert not predicates.is_hermitian(np.array([[3j]])) assert not predicates.is_hermitian(np.array([[0, 0]])) assert not predicates.is_hermitian(np.array([[0], [0]])) assert not predicates.is_hermitian(np.array([[5j, 0], [0, 2]])) assert predicates.is_hermitian(np.array([[5, 0], [0, 2]])) assert predicates.is_hermitian(np.array([[1, 0], [0, 1]])) assert not predicates.is_hermitian(np.array([[1, 0], [1, 1]])) assert not predicates.is_hermitian(np.array([[1, 1], [0, 1]])) assert predicates.is_hermitian(np.array([[1, 1], [1, 1]])) assert predicates.is_hermitian(np.array([[1, 1j], [-1j, 1]])) assert predicates.is_hermitian(np.array([[1, 1j], [-1j, 1]]) * np.sqrt(0.5)) assert not predicates.is_hermitian(np.array([[1, 1j], [1j, 1]])) assert not predicates.is_hermitian(np.array([[1, 0.1], [-0.1, 1]])) assert predicates.is_hermitian( np.array([[1, 1j + 1e-11], [-1j, 1 + 1j * 1e-9]]))
def diagonalize_real_symmetric_and_sorted_diagonal_matrices( symmetric_matrix: np.ndarray, diagonal_matrix: np.ndarray, tolerance: Tolerance = Tolerance.DEFAULT ) -> np.ndarray: """Returns an orthogonal matrix that diagonalizes both given matrices. The given matrices must commute. Guarantees that the sorted diagonal matrix is not permuted by the diagonalization (except for nearly-equal values). Args: symmetric_matrix: A real symmetric matrix. diagonal_matrix: A real diagonal matrix with entries along the diagonal sorted into descending order. tolerance: Numeric error thresholds. Returns: An orthogonal matrix P such that P.T @ symmetric_matrix @ P is diagonal and P.T @ diagonal_matrix @ P = diagonal_matrix (up to tolerance). Raises: ValueError: Matrices don't meet preconditions (e.g. not symmetric). ArithmeticError: Failed to meet specified tolerance. """ # Verify preconditions. if (np.any(np.imag(symmetric_matrix) != 0) or not predicates.is_hermitian(symmetric_matrix)): raise ValueError('symmetric_matrix must be real symmetric.') if (not predicates.is_diagonal(diagonal_matrix) or np.any(np.imag(diagonal_matrix) != 0) or np.any(diagonal_matrix[:-1, :-1] < diagonal_matrix[1:, 1:])): raise ValueError( 'diagonal_matrix must be real diagonal descending.') if not predicates.commutes(diagonal_matrix, symmetric_matrix): raise ValueError('Given matrices must commute.') def similar_singular(i, j): return tolerance.all_close(diagonal_matrix[i, i], diagonal_matrix[j, j]) # Because the symmetric matrix commutes with the diagonal singulars matrix, # the symmetric matrix should be block-diagonal with a block boundary # wherever the singular values happen change. So we can use the singular # values to extract blocks that can be independently diagonalized. ranges = _contiguous_groups(diagonal_matrix.shape[0], similar_singular) # Build the overall diagonalization by diagonalizing each block. p = np.zeros(symmetric_matrix.shape, dtype=np.float64) for start, end in ranges: block = symmetric_matrix[start:end, start:end] p[start:end, start:end] = diagonalize_real_symmetric_matrix(block) # Check acceptability vs tolerances. if (not predicates.is_diagonal(p.T.dot(symmetric_matrix).dot(p), tolerance) or not tolerance.all_close(diagonal_matrix, p.T.dot(diagonal_matrix).dot(p))): raise ArithmeticError('Failed to diagonalize to specified tolerance.') return p