def test_block_diag(): assert np.allclose(combinators.block_diag(), np.zeros((0, 0))) assert np.allclose(combinators.block_diag(np.array([[1, 2], [3, 4]])), np.array([[1, 2], [3, 4]])) assert np.allclose( combinators.block_diag(np.array([[1, 2], [3, 4]]), np.array([[4, 5, 6], [7, 8, 9], [10, 11, 12]])), np.array([[1, 2, 0, 0, 0], [3, 4, 0, 0, 0], [0, 0, 4, 5, 6], [0, 0, 7, 8, 9], [0, 0, 10, 11, 12]]))
def test_block_diag_dtype(): assert combinators.block_diag().dtype == np.complex128 assert (combinators.block_diag(np.array([[1]], dtype=np.int8)).dtype == np.int8) assert combinators.block_diag( np.array([[1]], dtype=np.float32), np.array([[2]], dtype=np.float32)).dtype == np.float32 assert combinators.block_diag( np.array([[1]], dtype=np.float64), np.array([[2]], dtype=np.float64)).dtype == np.float64 assert combinators.block_diag( np.array([[1]], dtype=np.float32), np.array([[2]], dtype=np.float64)).dtype == np.float64 assert combinators.block_diag( np.array([[1]], dtype=np.float32), np.array([[2]], dtype=np.complex64)).dtype == np.complex64 assert combinators.block_diag( np.array([[1]], dtype=np.int), np.array([[2]], dtype=np.complex128)).dtype == np.complex128
def test_block_diag_dtype(): assert combinators.block_diag().dtype == np.complex128 assert (combinators.block_diag(np.array([[1]], dtype=np.int8)).dtype == np.int8) assert combinators.block_diag(np.array([[1]], dtype=np.float32), np.array( [[2]], dtype=np.float32)).dtype == np.float32 assert combinators.block_diag(np.array([[1]], dtype=np.float64), np.array( [[2]], dtype=np.float64)).dtype == np.float64 assert combinators.block_diag(np.array([[1]], dtype=np.float32), np.array( [[2]], dtype=np.float64)).dtype == np.float64 assert combinators.block_diag( np.array([[1]], dtype=np.float32), np.array([[2]], dtype=np.complex64)).dtype == np.complex64 assert combinators.block_diag( np.array([[1]], dtype=np.int), np.array([[2]], dtype=np.complex128)).dtype == np.complex128
def test_block_diag(): assert np.allclose( combinators.block_diag(), np.zeros((0, 0))) assert np.allclose( combinators.block_diag( np.array([[1, 2], [3, 4]])), np.array([[1, 2], [3, 4]])) assert np.allclose( combinators.block_diag( np.array([[1, 2], [3, 4]]), np.array([[4, 5, 6], [7, 8, 9], [10, 11, 12]])), np.array([[1, 2, 0, 0, 0], [3, 4, 0, 0, 0], [0, 0, 4, 5, 6], [0, 0, 7, 8, 9], [0, 0, 10, 11, 12]]))
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 random_block_diagonal_symmetric_matrix(*ns: int) -> np.ndarray: return combinators.block_diag(*[random_symmetric_matrix(n) for n in ns])
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