def subspace_index( self, little_endian_bits_int: int, ) -> Tuple[Union[slice, int, 'ellipsis'], ...]: """An index for the subspace where the target axes equal a value. Args: little_endian_bits_int: The desired value of the qubits at the targeted `axes`, packed into an integer. The least significant bit of the integer is the desired bit for the first axis, and so forth in increasing order. value_tuple: The desired value of the qids at the targeted `axes`, packed into a tuple. Specify either `little_endian_bits_int` or `value_tuple`. Returns: A value that can be used to index into `target_tensor` and `available_buffer`, and manipulate only the part of Hilbert space corresponding to a given bit assignment. Example: If `target_tensor` is a 4 qubit tensor and `axes` is `[1, 3]` and then this method will return the following when given `little_endian_bits=0b01`: `(slice(None), 0, slice(None), 1, Ellipsis)` Therefore the following two lines would be equivalent: args.target_tensor[args.subspace_index(0b01)] += 1 args.target_tensor[:, 0, :, 1] += 1 """ return linalg.slice_for_qubits_equal_to(self.axes, little_endian_bits_int)
def _apply_unitary_(self, args: protocols.ApplyUnitaryArgs) -> np.ndarray: n = len(self.controls) control_axes = args.axes[:n] sub_axes = args.axes[n:] active = linalg.slice_for_qubits_equal_to(control_axes, -1) view_axes = _positions_after_removals_at(initial_positions=sub_axes, removals=control_axes) target_view = args.target_tensor[active] buffer_view = args.available_buffer[active] result = protocols.apply_unitary(self.sub_operation, protocols.ApplyUnitaryArgs( target_view, buffer_view, view_axes), default=NotImplemented) if result is NotImplemented: return NotImplemented if result is target_view: return args.target_tensor # HACK: assume they didn't somehow escape the slice view and edit the # rest of target_tensor. args.target_tensor[active] = result return args.target_tensor
def _probs(state: np.ndarray, indices: Sequence[int], qid_shape: Tuple[int, ...]) -> np.ndarray: """Returns the probabilities for a measurement on the given indices.""" tensor = np.reshape(state, qid_shape) # Calculate the probabilities for measuring the particular results. if len(indices) == len(qid_shape): # We're measuring every qudit, so no need for fancy indexing probs = np.abs(tensor) ** 2 probs = np.transpose(probs, indices) probs = np.reshape(probs, np.prod(probs.shape)) else: # Fancy indexing required meas_shape = tuple(qid_shape[i] for i in indices) probs = ( np.abs( [ tensor[ linalg.slice_for_qubits_equal_to( indices, big_endian_qureg_value=b, qid_shape=qid_shape ) ] for b in range(np.prod(meas_shape, dtype=int)) ] ) ** 2 ) probs = np.sum(probs, axis=tuple(range(1, len(probs.shape)))) # To deal with rounding issues, ensure that the probabilities sum to 1. probs /= np.sum(probs) return probs
def _apply_unitary_to_tensor_( self, target_tensor: np.ndarray, available_buffer: np.ndarray, axes: Sequence[int], ) -> Union[np.ndarray, type(NotImplemented)]: if self.exponent != 1: return NotImplemented zo = linalg.slice_for_qubits_equal_to(axes, 0b01) oz = linalg.slice_for_qubits_equal_to(axes, 0b10) available_buffer[zo] = target_tensor[zo] target_tensor[zo] = target_tensor[oz] target_tensor[oz] = available_buffer[zo] target_tensor[zo] *= 1j target_tensor[oz] *= 1j return target_tensor
def _apply_unitary_to_tensor_(self, target_tensor: np.ndarray, available_buffer: np.ndarray, axes: Sequence[int], ) -> Union[np.ndarray, NotImplementedType]: if self._exponent != 1: return NotImplemented zo = linalg.slice_for_qubits_equal_to(axes, 0b01) oz = linalg.slice_for_qubits_equal_to(axes, 0b10) available_buffer[zo] = target_tensor[zo] target_tensor[zo] = target_tensor[oz] target_tensor[oz] = available_buffer[zo] p = 1j**(2 * self._exponent * self._global_shift) if p != 1: target_tensor *= p return target_tensor
def subspace_index( self, axes: Sequence[int], little_endian_bits_int: int = 0, *, big_endian_bits_int: int = 0 ) -> Tuple[Union[slice, int, 'ellipsis'], ...]: """An index for the subspace where the target axes equal a value. Args: axes: The qubits that are specified by the index bits. little_endian_bits_int: The desired value of the qubits at the targeted `axes`, packed into an integer. The least significant bit of the integer is the desired bit for the first axis, and so forth in increasing order. Can't be specified at the same time as `big_endian_bits_int`. When operating on qudits instead of qubits, the same basic logic applies but in a different basis. For example, if the target axes have dimension [a:2, b:3, c:2] then the integer 10 decomposes into [a=0, b=2, c=1] via 7 = 1*(3*2) + 2*(2) + 0. big_endian_bits_int: The desired value of the qubits at the targeted `axes`, packed into an integer. The most significant bit of the integer is the desired bit for the first axis, and so forth in decreasing order. Can't be specified at the same time as `little_endian_bits_int`. When operating on qudits instead of qubits, the same basic logic applies but in a different basis. For example, if the target axes have dimension [a:2, b:3, c:2] then the integer 10 decomposes into [a=1, b=2, c=0] via 7 = 1*(3*2) + 2*(2) + 0. Returns: A value that can be used to index into `target_tensor` and `available_buffer`, and manipulate only the part of Hilbert space corresponding to a given bit assignment. Example: If `target_tensor` is a 4 qubit tensor and `axes` is `[1, 3]` and then this method will return the following when given `little_endian_bits=0b01`: `(slice(None), 0, slice(None), 1, Ellipsis)` Therefore the following two lines would be equivalent: args.target_tensor[args.subspace_index(0b01)] += 1 args.target_tensor[:, 0, :, 1] += 1 """ return linalg.slice_for_qubits_equal_to( axes, little_endian_qureg_value=little_endian_bits_int, big_endian_qureg_value=big_endian_bits_int, qid_shape=self.target_tensor.shape, )
def _apply_unitary_to_tensor_(self, target_tensor: np.ndarray, available_buffer: np.ndarray, axes: Sequence[int], ) -> np.ndarray: if protocols.is_parameterized(self): return NotImplemented ooo = linalg.slice_for_qubits_equal_to(axes, 0b111) target_tensor[ooo] *= np.exp(1j * self.exponent * np.pi) return target_tensor
def _apply_unitary_to_tensor_( self, target_tensor: np.ndarray, available_buffer: np.ndarray, axes: Sequence[int], ) -> Union[np.ndarray, NotImplementedType]: if protocols.is_parameterized(self): return NotImplemented global_phase = 1j**(2 * self._exponent * self._global_shift) if global_phase != 1: target_tensor *= global_phase relative_phase = 1j**(2 * self.exponent) zo = linalg.slice_for_qubits_equal_to(axes, 0b01) oz = linalg.slice_for_qubits_equal_to(axes, 0b10) target_tensor[oz] *= relative_phase target_tensor[zo] *= relative_phase return target_tensor
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_(self, args: protocols.ApplyUnitaryArgs ) -> Union[np.ndarray, NotImplementedType]: if protocols.is_parameterized(self): return NotImplemented c = 1j**(2 * self._exponent) one_one = linalg.slice_for_qubits_equal_to(args.axes, 0b11) args.target_tensor[one_one] *= c p = 1j**(2 * self._exponent * self._global_shift) if p != 1: args.target_tensor *= p return args.target_tensor
def _apply_unitary_to_tensor_( self, target_tensor: np.ndarray, available_buffer: np.ndarray, axes: Sequence[int], ) -> Union[np.ndarray, NotImplementedType]: if protocols.is_parameterized(self): return NotImplemented one = linalg.slice_for_qubits_equal_to(axes, 1) c = np.exp(1j * np.pi * self.half_turns) target_tensor[one] *= c return target_tensor
def _apply_unitary_to_tensor_( self, target_tensor: np.ndarray, available_buffer: np.ndarray, axes: Sequence[int], ) -> Union[np.ndarray, type(NotImplemented)]: if self.is_parameterized(): return NotImplemented c = np.exp(1j * np.pi * self.half_turns) one_one = linalg.slice_for_qubits_equal_to(axes, 0b11) target_tensor[one_one] *= c return target_tensor
def _probs(state: np.ndarray, indices: List[int], num_qubits: int) -> List[float]: """Returns the probabilities for a measurement on the given indices.""" # Tensor of squared amplitudes, shaped a rank [2, 2, .., 2] tensor. tensor = np.reshape(state, [2] * num_qubits) # Calculate the probabilities for measuring the particular results. probs = [ np.linalg.norm(tensor[linalg.slice_for_qubits_equal_to(indices, b)])**2 for b in range(2**len(indices)) ] # To deal with rounding issues, ensure that the probabilities sum to 1. probs /= sum(probs) # type: ignore return probs
def _apply_unitary_to_tensor_(self, target_tensor: np.ndarray, available_buffer: np.ndarray, axes: Sequence[int], ) -> Union[np.ndarray, NotImplementedType]: if protocols.is_parameterized(self): return NotImplemented c = 1j**(2 * self._exponent) one_one = linalg.slice_for_qubits_equal_to(axes, 0b11) target_tensor[one_one] *= c p = 1j**(2 * self._exponent * self._global_shift) if p != 1: target_tensor *= p return target_tensor
def assert_phase_by_is_consistent_with_unitary(val: Any): """Uses `val._unitary_` to check `val._phase_by_`'s behavior.""" original = protocols.unitary(val, None) if original is None: # If there's no unitary, it's vacuously consistent. return qid_shape = protocols.qid_shape(val, default=(2, ) * (len(original).bit_length() - 1)) original = original.reshape(qid_shape * 2) for t in [0.125, -0.25, 1, sympy.Symbol('a'), sympy.Symbol('a') + 1]: p = 1j**(t * 4) p = protocols.resolve_parameters(p, {'a': -0.125}) for i in range(len(qid_shape)): phased = protocols.phase_by(val, t, i, default=None) if phased is None: # If not phaseable, then phase_by is vacuously consistent. continue phased = protocols.resolve_parameters(phased, {'a': -0.125}) actual = protocols.unitary(phased).reshape(qid_shape * 2) expected = np.array(original) s = linalg.slice_for_qubits_equal_to([i], 1) expected[s] *= p s = linalg.slice_for_qubits_equal_to([len(qid_shape) + i], 1) expected[s] *= np.conj(p) lin_alg_utils.assert_allclose_up_to_global_phase( actual, expected, atol=1e-8, err_msg=f'Phased unitary was incorrect for index #{i}', )
def _probs(state: np.ndarray, indices: List[int], qid_shape: Tuple[int, ...]) -> List[float]: """Returns the probabilities for a measurement on the given indices.""" # Tensor of squared amplitudes, shaped a rank [2, 2, .., 2] tensor. tensor = np.reshape(state, qid_shape) # Calculate the probabilities for measuring the particular results. meas_shape = tuple(qid_shape[i] for i in indices) probs = [ np.linalg.norm(tensor[linalg.slice_for_qubits_equal_to( indices, big_endian_qureg_value=b, qid_shape=qid_shape)])**2 for b in range(np.prod(meas_shape, dtype=int)) ] # To deal with rounding issues, ensure that the probabilities sum to 1. probs /= sum(probs) # type: ignore return probs
def _probs(density_matrix: np.ndarray, indices: List[int], num_qubits: int) -> List[float]: """Returns the probabilities for a measurement on the given indices.""" # Only diagonal elements matter. all_probs = np.diagonal( np.reshape(density_matrix, (2**num_qubits, 2**num_qubits))) # Shape into a tensor tensor = np.reshape(all_probs, [2] * num_qubits) # Calculate the probabilities for measuring the particular results. probs = [ np.sum(np.abs(tensor[linalg.slice_for_qubits_equal_to(indices, b)])) for b in range(2**len(indices)) ] # To deal with rounding issues, ensure that the probabilities sum to 1. probs /= np.sum(probs) # type: ignore return probs
def _probs(density_matrix: np.ndarray, indices: List[int], qid_shape: Tuple[int, ...]) -> List[float]: """Returns the probabilities for a measurement on the given indices.""" # Only diagonal elements matter. all_probs = np.diagonal( np.reshape(density_matrix, (np.prod(qid_shape, dtype=int),) * 2)) # Shape into a tensor tensor = np.reshape(all_probs, qid_shape) # Calculate the probabilities for measuring the particular results. meas_shape = tuple(qid_shape[i] for i in indices) probs = [ np.sum( np.abs(tensor[linalg.slice_for_qubits_equal_to( indices, big_endian_qureg_value=b, qid_shape=qid_shape)])) for b in range(np.prod(meas_shape, dtype=int)) ] # To deal with rounding issues, ensure that the probabilities sum to 1. probs /= np.sum(probs) # type: ignore return probs
def _apply_unitary_(self, args: 'cirq.ApplyUnitaryArgs'): transposed_args = args.with_axes_transposed_to_start() target_axes = transposed_args.axes[:len(self.base_operation.qubits)] control_axes = transposed_args.axes[len(self.base_operation.qubits):] control_max = np.product([q.dimension for q in self.register]).item() for i in range(control_max): operation = self.base_operation**(self.exponent_sign * i / control_max) control_index = linalg.slice_for_qubits_equal_to( control_axes, big_endian_qureg_value=i) sub_args = cirq.ApplyUnitaryArgs( transposed_args.target_tensor[control_index], transposed_args.available_buffer[control_index], target_axes) sub_result = cirq.apply_unitary(operation, sub_args) if sub_result is not sub_args.target_tensor: sub_args.target_tensor[...] = sub_result return args.target_tensor
def measure_density_matrix(density_matrix: np.ndarray, indices: List[int], qid_shape: Optional[Tuple[int, ...]] = None, out: np.ndarray = None, seed: 'cirq.RANDOM_STATE_OR_SEED_LIKE' = None ) -> Tuple[List[int], np.ndarray]: """Performs a measurement of the density matrix in the computational basis. This does not modify `density_matrix` unless the optional `out` is `density_matrix`. Args: density_matrix: The density matrix to be measured. This matrix is assumed to be positive semidefinite and trace one. The matrix is assumed to be of shape (2 ** integer, 2 ** integer) or (2, 2, ..., 2). indices: Which qubits are measured. The matrix is assumed to be supplied in big endian order. That is the xth index of v, when expressed as a bitstring, has the largest values in the 0th index. qid_shape: The qid shape of the density matrix. Specify this argument when using qudits. out: An optional place to store the result. If `out` is the same as the `density_matrix` parameter, then `density_matrix` will be modified inline. If `out` is not None, then the result is put into `out`. If `out` is None a new value will be allocated. In all of these cases `out` will be the same as the returned ndarray of the method. The shape and dtype of `out` will match that of `density_matrix` if `out` is None, otherwise it will match the shape and dtype of `out`. seed: A seed for the pseudorandom number generator. Returns: A tuple of a list and an numpy array. The list is an array of booleans corresponding to the measurement values (ordered by the indices). The numpy array is the post measurement matrix. This matrix has the same shape and dtype as the input matrix. Raises: ValueError if the dimension of the matrix is not compatible with a matrix of n qubits. IndexError if the indices are out of range for the number of qubits corresponding to the density matrix. """ if qid_shape is None: num_qubits = _validate_num_qubits(density_matrix) qid_shape = (2,) * num_qubits else: _validate_density_matrix_qid_shape(density_matrix, qid_shape) num_qubits = len(qid_shape) meas_shape = _indices_shape(qid_shape, indices) if len(indices) == 0: if out is None: out = np.copy(density_matrix) elif out is not density_matrix: np.copyto(dst=out, src=density_matrix) return ([], out) # Final else: if out is matrix then matrix will be modified in place. prng = value.parse_random_state(seed) # Cache initial shape. initial_shape = density_matrix.shape # Calculate the measurement probabilities and then make the measurement. probs = _probs(density_matrix, indices, qid_shape) result = prng.choice(len(probs), p=probs) measurement_bits = value.big_endian_int_to_digits(result, base=meas_shape) # Calculate the slice for the measurement result. result_slice = linalg.slice_for_qubits_equal_to( indices, big_endian_qureg_value=result, qid_shape=qid_shape) # Create a mask which is False for only the slice. mask = np.ones(qid_shape * 2, dtype=bool) # Remove ellipses from last element of mask[result_slice * 2] = False if out is None: out = np.copy(density_matrix) elif out is not density_matrix: np.copyto(dst=out, src=density_matrix) # Final else: if out is matrix then matrix will be modified in place. # Potentially reshape to tensor, and then set masked values to 0. out.shape = qid_shape * 2 out[mask] = 0 # Restore original shape (if necessary) and renormalize. out.shape = initial_shape out /= probs[result] return measurement_bits, out
def measure_state_vector( state_vector: np.ndarray, indices: Sequence[int], *, # Force keyword args qid_shape: Optional[Tuple[int, ...]] = None, out: np.ndarray = None, seed: 'cirq.RANDOM_STATE_OR_SEED_LIKE' = None, ) -> Tuple[List[int], np.ndarray]: """Performs a measurement of the state in the computational basis. This does not modify `state` unless the optional `out` is `state`. Args: state_vector: The state to be measured. This state vector is assumed to be normalized. The state vector must be of size 2 ** integer. The state vector can be of shape (2 ** integer) or (2, 2, ..., 2). indices: Which qubits are measured. The `state_vector` is assumed to be supplied in big endian order. That is the xth index of v, when expressed as a bitstring, has the largest values in the 0th index. qid_shape: The qid shape of the `state_vector`. Specify this argument when using qudits. out: An optional place to store the result. If `out` is the same as the `state_vector` parameter, then `state_vector` will be modified inline. If `out` is not None, then the result is put into `out`. If `out` is None a new value will be allocated. In all of these case out will be the same as the returned ndarray of the method. The shape and dtype of `out` will match that of `state_vector` if `out` is None, otherwise it will match the shape and dtype of `out`. seed: A seed for the pseudorandom number generator. Returns: A tuple of a list and an numpy array. The list is an array of booleans corresponding to the measurement values (ordered by the indices). The numpy array is the post measurement state vector. This state vector has the same shape and dtype as the input `state_vector`. Raises: ValueError if the size of state is not a power of 2. IndexError if the indices are out of range for the number of qubits corresponding to the state. """ shape = qis.validate_qid_shape(state_vector, qid_shape) num_qubits = len(shape) qis.validate_indices(num_qubits, indices) if len(indices) == 0: if out is None: out = np.copy(state_vector) elif out is not state_vector: np.copyto(dst=out, src=state_vector) # Final else: if out is state then state will be modified in place. return ([], out) prng = value.parse_random_state(seed) # Cache initial shape. initial_shape = state_vector.shape # Calculate the measurement probabilities and then make the measurement. probs = _probs(state_vector, indices, shape) result = prng.choice(len(probs), p=probs) ###measurement_bits = [(1 & (result >> i)) for i in range(len(indices))] # Convert to individual qudit measurements. meas_shape = tuple(shape[i] for i in indices) measurement_bits = value.big_endian_int_to_digits(result, base=meas_shape) # Calculate the slice for the measurement result. result_slice = linalg.slice_for_qubits_equal_to( indices, big_endian_qureg_value=result, qid_shape=shape) # Create a mask which is False for only the slice. mask = np.ones(shape, dtype=bool) mask[result_slice] = False if out is None: out = np.copy(state_vector) elif out is not state_vector: np.copyto(dst=out, src=state_vector) # Final else: if out is state then state will be modified in place. # Potentially reshape to tensor, and then set masked values to 0. out.shape = shape out[mask] = 0 # Restore original shape (if necessary) and renormalize. out.shape = initial_shape out /= np.sqrt(probs[result]) return measurement_bits, out
def measure_density_matrix( density_matrix: np.ndarray, indices: List[int], out: np.ndarray = None) -> Tuple[List[bool], np.ndarray]: """Performs a measurement of the density matrix in the computational basis. This does not modify `density_matrix` unless the optional `out` is `density_matrix`. Args: density_matrix: The density matrix to be measured. This matrix is assumed to be positive semidefinite and trace one. The matrix is assumed to be of shape (2 ** integer, 2 ** integer) or (2, 2, ..., 2). indices: Which qubits are measured. The matrix is assumed to be supplied in big endian order. That is the xth index of v, when expressed as a bitstring, has the largest values in the 0th index. out: An optional place to store the result. If `out` is the same as the `density_matrix` parameter, then `density_matrix` will be modified inline. If `out` is not None, then the result is put into `out`. If `out` is None a new value will be allocated. In all of these cases `out` will be the same as the returned ndarray of the method. The shape and dtype of `out` will match that of `density_matrix` if `out` is None, otherwise it will match the shape and dtype of `out`. Returns: A tuple of a list and an numpy array. The list is an array of booleans corresponding to the measurement values (ordered by the indices). The numpy array is the post measurement matrix. This matrix has the same shape and dtype as the input matrix. Raises: ValueError if the dimension of the matrix is not compatible with a matrix of n qubits. IndexError if the indices are out of range for the number of qubits corresponding to the density matrix. """ num_qubits = _validate_num_qubits(density_matrix) _validate_indices(num_qubits, indices) if len(indices) == 0: if out is None: out = np.copy(density_matrix) elif out is not density_matrix: np.copyto(dst=out, src=density_matrix) return ([], out) # Final else: if out is matrix then matrix will be modified in place. # Cache initial shape. initial_shape = density_matrix.shape # Calculate the measurement probabilities and then make the measurement. probs = _probs(density_matrix, indices, num_qubits) result = np.random.choice(len(probs), p=probs) measurement_bits = [(1 & (result >> i)) for i in range(len(indices))] # Calculate the slice for the measurement result. result_slice = linalg.slice_for_qubits_equal_to(indices, result, num_qubits=num_qubits) # Create a mask which is False for only the slice. mask = np.ones([2] * 2 * num_qubits, dtype=bool) # Remove ellipses from last element of mask[result_slice * 2] = False if out is None: out = np.copy(density_matrix) elif out is not density_matrix: np.copyto(dst=out, src=density_matrix) # Final else: if out is matrix then matrix will be modified in place. # Potentially reshape to tensor, and then set masked values to 0. out.shape = [2] * num_qubits * 2 out[mask] = 0 # Restore original shape (if necessary) and renormalize. out.shape = initial_shape out /= probs[result] return measurement_bits, out
def measure_state_vector( state: np.ndarray, indices: List[int], out: np.ndarray = None) -> Tuple[List[bool], np.ndarray]: """Performs a measurement of the state in the computational basis. This does not modify `state` unless the optional `out` is `state`. Args: state: The state to be measured. This state is assumed to be normalized. The state must be of size 2 ** integer. The state can be of shape (2 ** integer) or (2, 2, ..., 2). indices: Which qubits are measured. The state is assumed to be supplied in big endian order. That is the xth index of v, when expressed as a bitstring, has the largest values in the 0th index. out: An optional place to store the result. If `out` is the same as the `state` parameter, then state will be modified inline. If `out` is not None, then the result is put into `out`. If `out` is None a new value will be allocated. In all of these case out will be the same as the returned ndarray of the method. The shape and dtype of `out` will match that of state if `out` is None, otherwise it will match the shape and dtype of `out`. Returns: A tuple of a list and an numpy array. The list is an array of booleans corresponding to the measurement values (ordered by the indices). The numpy array is the post measurement state. This state has the same shape and dtype as the input state. Raises: ValueError if the size of state is not a power of 2. IndexError if the indices are out of range for the number of qubits corresponding to the state. """ num_qubits = _validate_num_qubits(state) _validate_indices(num_qubits, indices) if len(indices) == 0: if out is None: out = np.copy(state) elif out is not state: np.copyto(dst=out, src=state) # Final else: if out is state then state will be modified in place. return ([], out) # Cache initial shape. initial_shape = state.shape # Calculate the measurement probabilities and then make the measurement. probs = _probs(state, indices, num_qubits) result = np.random.choice(len(probs), p=probs) measurement_bits = [(1 & (result >> i)) for i in range(len(indices))] # Calculate the slice for the measurement result. result_slice = linalg.slice_for_qubits_equal_to(indices, result) # Create a mask which is False for only the slice. mask = np.ones([2] * num_qubits, dtype=bool) mask[result_slice] = False if out is None: out = np.copy(state) elif out is not state: np.copyto(dst=out, src=state) # Final else: if out is state then state will be modified in place. # Potentially reshape to tensor, and then set masked values to 0. out.shape = [2] * num_qubits out[mask] = 0 # Restore original shape (if necessary) and renormalize. out.shape = initial_shape out /= np.sqrt(probs[result]) return measurement_bits, out
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)))