Ejemplo n.º 1
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])
Ejemplo n.º 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])
Ejemplo n.º 3
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(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
Ejemplo n.º 4
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)
Ejemplo n.º 5
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
Ejemplo n.º 6
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
Ejemplo n.º 7
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