Beispiel #1
0
    def _apply_unitary_(
            self, args: 'protocols.ApplyUnitaryArgs') -> Optional[np.ndarray]:
        if protocols.is_parameterized(self):
            return NotImplemented

        c = np.cos(np.pi * self._exponent / 2)
        s = np.sin(np.pi * self._exponent / 2)
        f = np.exp(2j * np.pi * self._phase_exponent)
        matrix = np.array([[c, 1j * s * f], [1j * s * f.conjugate(), c]])

        zo = args.subspace_index(0b01)
        oz = args.subspace_index(0b10)
        linalg.apply_matrix_to_slices(args.target_tensor,
                                      matrix, [oz, zo],
                                      out=args.available_buffer)
        return args.available_buffer
Beispiel #2
0
def _strat_apply_unitary_from_unitary(
        unitary_value: Any, args: ApplyUnitaryArgs) -> Optional[np.ndarray]:
    # Check for magic method.
    method = getattr(unitary_value, '_unitary_', None)
    if method is None:
        return NotImplemented

    # Attempt to get the unitary matrix.
    matrix = method()
    if matrix is NotImplemented or matrix is None:
        return matrix

    val_qid_shape = qid_shape_protocol.qid_shape(unitary_value,
                                                 default=(2, ) *
                                                 len(args.axes))
    sub_args = args._for_operation_with_qid_shape(range(len(val_qid_shape)),
                                                  val_qid_shape)
    matrix = matrix.astype(sub_args.target_tensor.dtype)
    if len(val_qid_shape) == 1 and val_qid_shape[0] <= 2:
        # Special case for single-qubit, 2x2 or 1x1 operations.
        # np.einsum is faster for larger cases.
        subspaces = [(..., level) for level in range(val_qid_shape[0])]
        sub_result = linalg.apply_matrix_to_slices(
            sub_args.target_tensor,
            matrix,
            subspaces,
            out=sub_args.available_buffer)
    else:
        # General case via np.einsum.
        sub_result = linalg.targeted_left_multiply(
            matrix.reshape(val_qid_shape * 2),
            sub_args.target_tensor,
            sub_args.axes,
            out=sub_args.available_buffer)
    return _incorporate_result_into_target(args, sub_args, sub_result)
Beispiel #3
0
def _strat_apply_unitary_from_unitary(
        unitary_value: Any, args: ApplyUnitaryArgs) -> Optional[np.ndarray]:
    # Check for magic method.
    method = getattr(unitary_value, '_unitary_', None)
    if method is None:
        return NotImplemented

    # Attempt to get the unitary matrix.
    matrix = method()
    if matrix is NotImplemented or matrix is None:
        return matrix

    # Special case for single-qubit operations.
    if matrix.shape == (2, 2):
        zero = args.subspace_index(0)
        one = args.subspace_index(1)
        return linalg.apply_matrix_to_slices(args.target_tensor,
                                             matrix, [zero, one],
                                             out=args.available_buffer)

    # General case via np.einsum.
    return linalg.targeted_left_multiply(matrix.astype(
        args.target_tensor.dtype).reshape((2, ) * (2 * len(args.axes))),
                                         args.target_tensor,
                                         args.axes,
                                         out=args.available_buffer)
Beispiel #4
0
def _apply_kraus_single_qubit(kraus: Union[Tuple[Any], Sequence[Any]],
                              args: 'ApplyChannelArgs') -> np.ndarray:
    """Use slicing to apply single qubit channel.  Only for two-level qubits."""
    zero_left = linalg.slice_for_qubits_equal_to(args.left_axes, 0)
    one_left = linalg.slice_for_qubits_equal_to(args.left_axes, 1)
    zero_right = linalg.slice_for_qubits_equal_to(args.right_axes, 0)
    one_right = linalg.slice_for_qubits_equal_to(args.right_axes, 1)
    for kraus_op in kraus:
        np.copyto(dst=args.target_tensor, src=args.auxiliary_buffer0)
        linalg.apply_matrix_to_slices(args.target_tensor,
                                      kraus_op, [zero_left, one_left],
                                      out=args.auxiliary_buffer1)
        # No need to transpose as we are acting on the tensor
        # representation of matrix, so transpose is done for us.
        linalg.apply_matrix_to_slices(args.auxiliary_buffer1,
                                      np.conjugate(kraus_op),
                                      [zero_right, one_right],
                                      out=args.target_tensor)
        args.out_buffer += args.target_tensor
    return args.out_buffer
Beispiel #5
0
def apply_unitary(
    unitary_value: Any,
    args: ApplyUnitaryArgs,
    default: TDefault = RaiseTypeErrorIfNotProvided
) -> Union[np.ndarray, TDefault]:
    """High performance left-multiplication of a unitary effect onto a tensor.

    If `unitary_value` defines an `_apply_unitary_` method, that method will be
    used to apply `unitary_value`'s unitary effect to the target tensor.
    Otherwise, if `unitary_value` defines a `_unitary_` method, its unitary
    matrix will be retrieved and applied using a generic method. Otherwise the
    application fails, and either an exception is raised or the specified
    default value is returned.

    Args:
        unitary_value: The value with a unitary effect to apply to the target.
        args: A mutable `cirq.ApplyUnitaryArgs` object describing the target
            tensor, available workspace, and axes to operate on. The attributes
            of this object will be mutated as part of computing the result.
        default: What should be returned if `unitary_value` doesn't have a
            unitary effect. If not specified, a TypeError is raised instead of
            returning a default value.

    Returns:
        If the receiving object is not able to apply its unitary effect,
        the specified default value is returned (or a TypeError is raised). If
        this occurs, then `target_tensor` should not have been mutated.

        If the receiving object was able to work inline, directly
        mutating target_tensor it will return target_tensor. The caller is
        responsible for checking if the result is target_tensor.

        If the receiving object wrote its output over available_buffer, the
        result will be available_buffer. The caller is responsible for
        checking if the result is available_buffer (and e.g. swapping
        the buffer for the target tensor before the next call).

        The receiving object may also write its output over a new buffer
        that it created, in which case that new array is returned.

    Raises:
        TypeError: `unitary_value` doesn't have a unitary effect and `default`
            wasn't specified.
    """

    # Check if the specialized method is present.
    func = getattr(unitary_value, '_apply_unitary_', None)
    if func is not None:
        result = func(args)
        if result is not NotImplemented and result is not None:
            return result

    # Fallback to using the object's _unitary_ matrix.
    matrix = unitary(unitary_value, None)
    if matrix is not None:
        # Special case for single-qubit operations.
        if matrix.shape == (2, 2):
            zero = args.subspace_index(0)
            one = args.subspace_index(1)
            return linalg.apply_matrix_to_slices(args.target_tensor,
                                                 matrix, [zero, one],
                                                 out=args.available_buffer)

        # Fallback to np.einsum for the general case.
        return linalg.targeted_left_multiply(matrix.astype(
            args.target_tensor.dtype).reshape((2, ) * (2 * len(args.axes))),
                                             args.target_tensor,
                                             args.axes,
                                             out=args.available_buffer)

    # Don't know how to apply. Fallback to specified default behavior.
    if default is not RaiseTypeErrorIfNotProvided:
        return default
    raise TypeError(
        "object of type '{}' has no _apply_unitary_ or _unitary_ methods "
        "(or they returned None or NotImplemented).".format(
            type(unitary_value)))
Beispiel #6
0
def apply_unitary_to_tensor(
    val: Any,
    target_tensor: np.ndarray,
    available_buffer: np.ndarray,
    axes: Sequence[int],
    default: TDefault = RaiseTypeErrorIfNotProvided
) -> Union[np.ndarray, TDefault]:
    """High performance left-multiplication of a unitary effect onto a tensor.

    If `val` defines an _apply_unitary_to_tensor_ method, that method will be
    used to apply `val`'s unitary effect to the target tensor. Otherwise, if
    `val` defines a _unitary_ method, its unitary matrix will be retrieved and
    applied using a generic method. Otherwise the application fails, and either
    an exception is raised or the specified default value is returned.

        The target may represent a wavefunction, a unitary matrix, or some other
        tensor. Implementations will work in all of these cases as long as they
        correctly focus on only operating on the given axes. See also:
        `cirq.slice_for_qubits_equal_to(axes, int)`, which does the correct
        thing in all these cases.

    Args:
        val: The value with a unitary effect to apply to the target tensor.
        target_tensor: The input tensor that needs to be left-multiplied by
            the unitary effect of `val`. Note that this value may be mutated
            inline into the output. The tensor will have the shape
            (2, 2, 2, ..., 2). target_tensor may correspond to a multi-qubit
            superposition (with each axis being a qubit), a multi-qubit unitary
            transformation (with some axes being qubit inputs and others being
            qubit outputs), or some other concept.
        available_buffer: Pre-allocated workspace with the same shape and
            dtype as the target tensor. Note that the output may be written
            into this buffer.
        axes: Which axes the unitary effect is being applied to (e.g. the
            qubits that the gate is operating on). For example, a CNOT being
            applied to qubits #4 and #2 of a circuit would result in
            axes=(4, 2).
        default: What should be returned if `val` doesn't have a unitary effect.
            If not specified, a TypeError is raised instead of returning
            a default value.

    Returns:
        If the receiving object is not able to apply its unitary effect,
        the specified default value is returned (or a TypeError is raised).

        If the receiving object was able to work inline, directly
        mutating target_tensor it will return target_tensor. The caller is
        responsible for checking if the result is target_tensor.

        If the receiving object wrote its output over available_buffer, the
        result will be available_buffer. The caller is responsible for
        checking if the result is available_buffer (and e.g. swapping
        the buffer for the target tensor before the next call).

        The receiving object may also write its output over a new buffer
        that it created, in which case that new array is returned.

    Raises:
        TypeError: `val` doesn't have a unitary effect and `default` wasn't
            specified.
    """

    # Check if the specialized method is present.
    getter = getattr(val, '_apply_unitary_to_tensor_', None)
    if getter is not None:
        result = getter(target_tensor, available_buffer, axes)
        if result is not NotImplemented:
            return result

    # Fallback to using the object's _unitary_ matrix.
    matrix = unitary(val, None)
    if matrix is not None:
        # Special case for single-qubit operations.
        if matrix.shape == (2, 2):
            zero = linalg.slice_for_qubits_equal_to(axes, 0)
            one = linalg.slice_for_qubits_equal_to(axes, 1)
            return linalg.apply_matrix_to_slices(target_tensor,
                                                 matrix, [zero, one],
                                                 out=available_buffer)

        # Fallback to np.einsum for the general case.
        return linalg.targeted_left_multiply(matrix.astype(
            target_tensor.dtype).reshape((2, ) * (2 * len(axes))),
                                             target_tensor,
                                             axes,
                                             out=available_buffer)

    # Don't know how to apply. Fallback to specified default behavior.
    if default is not RaiseTypeErrorIfNotProvided:
        return default
    raise TypeError("object of type '{}' "
                    "has no _apply_unitary_to_tensor_ "
                    "or _unitary_ methods "
                    "(or they returned NotImplemented).".format(type(val)))