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