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
Exemple #2
0
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])
Exemple #3
0
def test_is_diagonal_tolerance():
    tol = Tolerance(atol=0.5)

    # Pays attention to specified tolerance.
    assert predicates.is_diagonal(np.array([[1, 0], [-0.5, 1]]), tol)
    assert not predicates.is_diagonal(np.array([[1, 0], [-0.6, 1]]), tol)

    # Error isn't accumulated across entries.
    assert predicates.is_diagonal(np.array([[1, 0.5], [-0.5, 1]]), tol)
    assert not predicates.is_diagonal(np.array([[1, 0.5], [-0.6, 1]]), tol)
Exemple #4
0
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
Exemple #5
0
def test_is_unitary_tolerance():
    tol = Tolerance(atol=0.5)

    # Pays attention to specified tolerance.
    assert predicates.is_unitary(np.array([[1, 0], [-0.5, 1]]), tol)
    assert not predicates.is_unitary(np.array([[1, 0], [-0.6, 1]]), tol)

    # Error isn't accumulated across entries.
    assert predicates.is_unitary(
        np.array([[1.2, 0, 0], [0, 1.2, 0], [0, 0, 1.2]]), tol)
    assert not predicates.is_unitary(
        np.array([[1.2, 0, 0], [0, 1.3, 0], [0, 0, 1.2]]), tol)
Exemple #6
0
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])
Exemple #7
0
def kron_factor_4x4_to_2x2s(
        matrix: np.ndarray,
        tolerance: Tolerance = Tolerance.DEFAULT
) -> Tuple[complex, np.ndarray, np.ndarray]:
    """Splits a 4x4 matrix U = kron(A, B) into A, B, and a global factor.

    Requires the matrix to be the kronecker product of two 2x2 unitaries.
    Requires the matrix to have a non-zero determinant.

    Args:
        matrix: The 4x4 unitary matrix to factor.
        tolerance: Acceptable numeric error thresholds.

    Returns:
        A scalar factor and a pair of 2x2 unit-determinant matrices. The
        kronecker product of all three is equal to the given matrix.

    Raises:
        ValueError:
            The given matrix can't be tensor-factored into 2x2 pieces.
    """

    # Use the entry with the largest magnitude as a reference point.
    a, b = max(
        ((i, j) for i in range(4) for j in range(4)),
        key=lambda t: abs(matrix[t]))

    # Extract sub-factors touching the reference cell.
    f1 = np.zeros((2, 2), dtype=np.complex128)
    f2 = np.zeros((2, 2), dtype=np.complex128)
    for i in range(2):
        for j in range(2):
            f1[(a >> 1) ^ i, (b >> 1) ^ j] = matrix[a ^ (i << 1), b ^ (j << 1)]
            f2[(a & 1) ^ i, (b & 1) ^ j] = matrix[a ^ i, b ^ j]

    # Rescale factors to have unit determinants.
    f1 /= (np.sqrt(np.linalg.det(f1)) or 1)
    f2 /= (np.sqrt(np.linalg.det(f2)) or 1)

    # Determine global phase.
    g = matrix[a, b] / (f1[a >> 1, b >> 1] * f2[a & 1, b & 1])
    if np.real(g) < 0:
        f1 *= -1
        g = -g

    restored = g * combinators.kron(f1, f2)
    if np.any(np.isnan(restored)) or not tolerance.all_close(restored, matrix):
        raise ValueError("Can't factor into kronecker product.")

    return g, f1, f2
Exemple #8
0
def kron_factor_4x4_to_2x2s(
        matrix: np.ndarray,
        tolerance: Tolerance = Tolerance.DEFAULT
) -> Tuple[complex, np.ndarray, np.ndarray]:
    """Splits a 4x4 matrix U = kron(A, B) into A, B, and a global factor.

    Requires the matrix to be the kronecker product of two 2x2 unitaries.
    Requires the matrix to have a non-zero determinant.

    Args:
        matrix: The 4x4 unitary matrix to factor.
        tolerance: Acceptable numeric error thresholds.

    Returns:
        A scalar factor and a pair of 2x2 unit-determinant matrices. The
        kronecker product of all three is equal to the given matrix.

    Raises:
        ValueError:
            The given matrix can't be tensor-factored into 2x2 pieces.
    """

    # Use the entry with the largest magnitude as a reference point.
    a, b = max(
        ((i, j) for i in range(4) for j in range(4)),
        key=lambda t: abs(matrix[t]))

    # Extract sub-factors touching the reference cell.
    f1 = np.zeros((2, 2), dtype=np.complex128)
    f2 = np.zeros((2, 2), dtype=np.complex128)
    for i in range(2):
        for j in range(2):
            f1[(a >> 1) ^ i, (b >> 1) ^ j] = matrix[a ^ (i << 1), b ^ (j << 1)]
            f2[(a & 1) ^ i, (b & 1) ^ j] = matrix[a ^ i, b ^ j]

    # Rescale factors to have unit determinants.
    f1 /= (np.sqrt(np.linalg.det(f1)) or 1)
    f2 /= (np.sqrt(np.linalg.det(f2)) or 1)

    # Determine global phase.
    g = matrix[a, b] / (f1[a >> 1, b >> 1] * f2[a & 1, b & 1])
    if np.real(g) < 0:
        f1 *= -1
        g = -g

    restored = g * combinators.kron(f1, f2)
    if np.any(np.isnan(restored)) or not tolerance.all_close(restored, matrix):
        raise ValueError("Can't factor into kronecker product.")

    return g, f1, f2
Exemple #9
0
def is_hermitian(matrix: np.ndarray,
                 tolerance: Tolerance = Tolerance.DEFAULT) -> bool:
    """Determines if a matrix is approximately Hermitian.

    A matrix is Hermitian if it's square and equal to its adjoint.

    Args:
        matrix: The matrix to check.
        tolerance: The per-matrix-entry tolerance on equality.

    Returns:
        Whether the matrix is Hermitian within the given tolerance.
    """
    return (matrix.shape[0] == matrix.shape[1]
            and tolerance.all_close(matrix, np.conj(matrix.T)))
Exemple #10
0
def is_unitary(matrix: np.ndarray,
               tolerance: Tolerance = Tolerance.DEFAULT) -> bool:
    """Determines if a matrix is approximately unitary.

    A matrix is unitary if it's square and its adjoint is its inverse.

    Args:
        matrix: The matrix to check.
        tolerance: The per-matrix-entry tolerance on equality.

    Returns:
        Whether the matrix is unitary within the given tolerance.
    """
    return (matrix.shape[0] == matrix.shape[1] and tolerance.all_close(
        matrix.dot(np.conj(matrix.T)), np.eye(matrix.shape[0])))
Exemple #11
0
def test_is_special_orthogonal_tolerance():
    tol = Tolerance(atol=0.5)

    # Pays attention to specified tolerance.
    assert predicates.is_special_orthogonal(
        np.array([[1, 0], [-0.5, 1]]), tol)
    assert not predicates.is_special_orthogonal(
        np.array([[1, 0], [-0.6, 1]]), tol)

    # Error isn't accumulated across entries, except for determinant factors.
    assert predicates.is_special_orthogonal(
        np.array([[1.2, 0, 0], [0, 1.2, 0], [0, 0, 1 / 1.2]]), tol)
    assert not predicates.is_special_orthogonal(
        np.array([[1.2, 0, 0], [0, 1.2, 0], [0, 0, 1.2]]), tol)
    assert not predicates.is_special_orthogonal(
        np.array([[1.2, 0, 0], [0, 1.3, 0], [0, 0, 1 / 1.2]]), tol)
Exemple #12
0
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)
Exemple #13
0
def is_orthogonal(matrix: np.ndarray,
                  tolerance: Tolerance = Tolerance.DEFAULT) -> bool:
    """Determines if a matrix is approximately orthogonal.

    A matrix is orthogonal if it's square and real and its transpose is its
    inverse.

    Args:
        matrix: The matrix to check.
        tolerance: The per-matrix-entry tolerance on equality.

    Returns:
        Whether the matrix is orthogonal within the given tolerance.
    """
    return (matrix.shape[0] == matrix.shape[1]
            and np.all(np.imag(matrix) == 0) and tolerance.all_close(
                matrix.dot(matrix.T), np.eye(matrix.shape[0])))
Exemple #14
0
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)
Exemple #15
0
def is_special_orthogonal(matrix: np.ndarray,
                          tolerance: Tolerance = Tolerance.DEFAULT) -> bool:
    """Determines if a matrix is approximately special orthogonal.

    A matrix is special orthogonal if it is square and real and its transpose
    is its inverse and its determinant is one.

    Args:
        matrix: The matrix to check.
        tolerance: The per-matrix-entry tolerance on equality.

    Returns:
        Whether the matrix is special orthogonal within the given tolerance.
    """
    return (is_orthogonal(matrix, tolerance)
            and (matrix.shape[0] == 0
                 or tolerance.all_close(np.linalg.det(matrix), 1)))
Exemple #16
0
def so4_to_magic_su2s(
        mat: np.ndarray,
        tolerance: Tolerance = Tolerance.DEFAULT
) -> Tuple[np.ndarray, np.ndarray]:
    """Finds 2x2 special-unitaries A, B where mat = Mag.H @ kron(A, B) @ Mag.

    Mag is the magic basis matrix:

        1  0  0  i
        0  i  1  0
        0  i -1  0     (times sqrt(0.5) to normalize)
        1  0  0 -i

    Args:
        mat: A real 4x4 orthogonal matrix.
        tolerance: Per-matrix-entry tolerance on equality.

    Returns:
        A pair (A, B) of matrices in SU(2) such that Mag.H @ kron(A, B) @ Mag
        is approximately equal to the given matrix.

    Raises:
        ValueError: Bad matrix.
        ArithmeticError: Failed to perform the decomposition to desired
            tolerance.
        """
    if mat.shape != (4, 4) or not predicates.is_special_orthogonal(mat,
                                                                   tolerance):
        raise ValueError('mat must be 4x4 special orthogonal.')

    magic = np.array([[1, 0, 0, 1j],
                      [0, 1j, 1, 0],
                      [0, 1j, -1, 0],
                      [1, 0, 0, -1j]]) * np.sqrt(0.5)
    ab = combinators.dot(magic, mat, np.conj(magic.T))
    _, a, b = kron_factor_4x4_to_2x2s(ab, tolerance)

    # Check decomposition against desired tolerance.
    reconstructed = combinators.dot(np.conj(magic.T),
                                    combinators.kron(a, b),
                                    magic)
    if not tolerance.all_close(reconstructed, mat):
        raise ArithmeticError('Failed to decompose to desired tolerance.')

    return a, b
Exemple #17
0
def so4_to_magic_su2s(
        mat: np.ndarray,
        tolerance: Tolerance = Tolerance.DEFAULT
) -> Tuple[np.ndarray, np.ndarray]:
    """Finds 2x2 special-unitaries A, B where mat = Mag.H @ kron(A, B) @ Mag.

    Mag is the magic basis matrix:

        1  0  0  i
        0  i  1  0
        0  i -1  0     (times sqrt(0.5) to normalize)
        1  0  0 -i

    Args:
        mat: A real 4x4 orthogonal matrix.
        tolerance: Per-matrix-entry tolerance on equality.

    Returns:
        A pair (A, B) of matrices in SU(2) such that Mag.H @ kron(A, B) @ Mag
        is approximately equal to the given matrix.

    Raises:
        ValueError: Bad matrix.
        ArithmeticError: Failed to perform the decomposition to desired
            tolerance.
        """
    if mat.shape != (4, 4) or not predicates.is_special_orthogonal(mat,
                                                                   tolerance):
        raise ValueError('mat must be 4x4 special orthogonal.')

    magic = np.array([[1, 0, 0, 1j],
                    [0, 1j, 1, 0],
                    [0, 1j, -1, 0],
                    [1, 0, 0, -1j]]) * np.sqrt(0.5)
    ab = combinators.dot(magic, mat, np.conj(magic.T))
    _, a, b = kron_factor_4x4_to_2x2s(ab, tolerance)

    # Check decomposition against desired tolerance.
    reconstructed = combinators.dot(np.conj(magic.T),
                                    combinators.kron(a, b),
                                    magic)
    if not tolerance.all_close(reconstructed, mat):
        raise ArithmeticError('Failed to decompose to desired tolerance.')

    return a, b
Exemple #18
0
def is_special_unitary(matrix: np.ndarray,
                       tolerance: Tolerance = Tolerance.DEFAULT) -> bool:
    """Determines if a matrix is approximately unitary with unit determinant.

    A matrix is special-unitary if it is square and its adjoint is its inverse
    and its determinant is one.

    Args:
        matrix: The matrix to check.
        tolerance: The per-matrix-entry tolerance on equality.

    Returns:
        Whether the matrix is unitary with unit determinant within the given
        tolerance.
    """
    return (is_unitary(matrix, tolerance)
            and (matrix.shape[0] == 0
                 or tolerance.all_close(np.linalg.det(matrix), 1)))
Exemple #19
0
def commutes(m1: np.ndarray,
             m2: np.ndarray,
             tolerance: Tolerance = Tolerance.DEFAULT) -> bool:
    """Determines if two matrices approximately commute.

    Two matrices A and B commute if they are square and have the same size and
    AB = BA.

    Args:
        m1: One of the matrices.
        m2: The other matrix.
        tolerance: The per-matrix-entry tolerance on equality.

    Returns:
        Whether the two matrices have compatible sizes and a commutator equal
        to zero within tolerance.
  """
    return (m1.shape[0] == m1.shape[1] and m1.shape == m2.shape
            and tolerance.all_close(m1.dot(m2), m2.dot(m1)))
Exemple #20
0
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
Exemple #21
0
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
Exemple #22
0
def test_all_close():
    no_tol = Tolerance()
    assert no_tol.all_close(1, 1)
    assert not no_tol.all_close(1, 0.5)
    assert not no_tol.all_close(1, 1.5)
    assert not no_tol.all_close(1, 100.5)
    assert no_tol.all_close(100, 100)
    assert not no_tol.all_close(100, 99.5)
    assert not no_tol.all_close(100, 100.5)

    assert no_tol.all_close([1, 2], [1, 2])
    assert not no_tol.all_close([1, 2], [1, 3])

    atol5 = Tolerance(atol=5)
    assert atol5.all_close(1, 1)
    assert atol5.all_close(1, 0.5)
    assert atol5.all_close(1, 1.5)
    assert atol5.all_close(1, 5.5)
    assert atol5.all_close(1, -3.5)
    assert not atol5.all_close(1, 6.5)
    assert not atol5.all_close(1, -4.5)
    assert not atol5.all_close(1, 100.5)
    assert atol5.all_close(100, 100)
    assert atol5.all_close(100, 100.5)
    assert atol5.all_close(100, 104.5)
    assert not atol5.all_close(100, 105.5)

    rtol2 = Tolerance(rtol=2)
    assert rtol2.all_close(100, 100)
    assert rtol2.all_close(299.5, 100)
    assert rtol2.all_close(1, 100)
    assert rtol2.all_close(-99, 100)
    assert not rtol2.all_close(100, 1)  # Doesn't commute.
    assert not rtol2.all_close(300.5, 100)
    assert not rtol2.all_close(-101, 100)

    tol25 = Tolerance(rtol=2, atol=5)
    assert tol25.all_close(100, 100)
    assert tol25.all_close(106, 100)
    assert tol25.all_close(201, 100)
    assert tol25.all_close(304.5, 100)
    assert not tol25.all_close(305.5, 100)
Exemple #23
0
def test_all_close():
    no_tol = Tolerance()
    assert no_tol.all_close(1, 1)
    assert not no_tol.all_close(1, 0.5)
    assert not no_tol.all_close(1, 1.5)
    assert not no_tol.all_close(1, 100.5)
    assert no_tol.all_close(100, 100)
    assert not no_tol.all_close(100, 99.5)
    assert not no_tol.all_close(100, 100.5)

    assert no_tol.all_close([1, 2], [1, 2])
    assert not no_tol.all_close([1, 2], [1, 3])

    atol5 = Tolerance(atol=5)
    assert atol5.all_close(1, 1)
    assert atol5.all_close(1, 0.5)
    assert atol5.all_close(1, 1.5)
    assert atol5.all_close(1, 5.5)
    assert atol5.all_close(1, -3.5)
    assert not atol5.all_close(1, 6.5)
    assert not atol5.all_close(1, -4.5)
    assert not atol5.all_close(1, 100.5)
    assert atol5.all_close(100, 100)
    assert atol5.all_close(100, 100.5)
    assert atol5.all_close(100, 104.5)
    assert not atol5.all_close(100, 105.5)

    rtol2 = Tolerance(rtol=2)
    assert rtol2.all_close(100, 100)
    assert rtol2.all_close(299.5, 100)
    assert rtol2.all_close(1, 100)
    assert rtol2.all_close(-99, 100)
    assert not rtol2.all_close(100, 1)  # Doesn't commute.
    assert not rtol2.all_close(300.5, 100)
    assert not rtol2.all_close(-101, 100)

    tol25 = Tolerance(rtol=2, atol=5)
    assert tol25.all_close(100, 100)
    assert tol25.all_close(106, 100)
    assert tol25.all_close(201, 100)
    assert tol25.all_close(304.5, 100)
    assert not tol25.all_close(305.5, 100)
Exemple #24
0
def test_repr():
    assert (repr(Tolerance(rtol=2, atol=3, equal_nan=True)) ==
            'Tolerance(rtol=2, atol=3, equal_nan=True)')
    assert (str(Tolerance(rtol=5, atol=6, equal_nan=False)) ==
            'Tolerance(rtol=5, atol=6, equal_nan=False)')
Exemple #25
0
def test_all_zero_mod():
    tol = Tolerance(atol=5, rtol=9999999)
    assert tol.all_near_zero_mod(0, 100)
    assert tol.all_near_zero_mod(4.5, 100)
    assert not tol.all_near_zero_mod(5.5, 100)

    assert tol.all_near_zero_mod(100, 100)
    assert tol.all_near_zero_mod(95.5, 100)
    assert not tol.all_near_zero_mod(94.5, 100)

    assert tol.all_near_zero_mod(-4.5, 100)
    assert not tol.all_near_zero_mod(-5.5, 100)

    assert tol.all_near_zero_mod(104.5, 100)
    assert not tol.all_near_zero_mod(105.5, 100)

    assert tol.all_near_zero_mod([-4.5, 0, 1, 4.5, 3, 95.5, 104.5], 100)
    assert not tol.all_near_zero_mod([-4.5, 0, 1, 4.5, 30], 100)
Exemple #26
0
def test_near_zero_mod():
    tol = Tolerance(atol=5, rtol=9999999)
    assert tol.near_zero_mod(0, 100)
    assert tol.near_zero_mod(4.5, 100)
    assert not tol.near_zero_mod(5.5, 100)

    assert tol.near_zero_mod(100, 100)
    assert tol.near_zero_mod(95.5, 100)
    assert not tol.near_zero_mod(94.5, 100)

    assert tol.near_zero_mod(-4.5, 100)
    assert not tol.near_zero_mod(-5.5, 100)

    assert tol.near_zero_mod(104.5, 100)
    assert not tol.near_zero_mod(105.5, 100)
Exemple #27
0
def test_near_zero():
    tol = Tolerance(atol=5, rtol=9999999)
    assert tol.near_zero(0)
    assert tol.near_zero(4.5)
    assert not tol.near_zero(5.5)
Exemple #28
0
def test_all_zero_mod():
    tol = Tolerance(atol=5, rtol=9999999)
    assert tol.all_near_zero_mod(0, 100)
    assert tol.all_near_zero_mod(4.5, 100)
    assert not tol.all_near_zero_mod(5.5, 100)

    assert tol.all_near_zero_mod(100, 100)
    assert tol.all_near_zero_mod(95.5, 100)
    assert not tol.all_near_zero_mod(94.5, 100)

    assert tol.all_near_zero_mod(-4.5, 100)
    assert not tol.all_near_zero_mod(-5.5, 100)

    assert tol.all_near_zero_mod(104.5, 100)
    assert not tol.all_near_zero_mod(105.5, 100)

    assert tol.all_near_zero_mod([-4.5, 0, 1, 4.5, 3, 95.5, 104.5], 100)
    assert not tol.all_near_zero_mod([-4.5, 0, 1, 4.5, 30], 100)
Exemple #29
0
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