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
示例#2
0
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
示例#3
0
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
示例#4
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
示例#5
0
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
示例#6
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)
示例#7
0
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
示例#10
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
示例#11
0
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]]))
示例#12
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
示例#13
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