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.

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)))