# Copyright 2018-2022 Xanadu Quantum Technologies Inc.# Licensed under the Apache License, Version 2.0 (the "License");# you may not use this file except in compliance with the License.# You may obtain a copy of the License at# http://www.apache.org/licenses/LICENSE-2.0# Unless required by applicable law or agreed to in writing, software# distributed under the License is distributed on an "AS IS" BASIS,# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.# See the License for the specific language governing permissions and# limitations under the License."""This module contains the :class:`QutritDevice` abstract base class."""# For now, arguments may be different from the signatures provided in QubitDevice to minimize size of pull request# e.g. instead of expval(self, observable, wires, par) have expval(self, observable)# pylint: disable=arguments-differ, abstract-method, no-value-for-parameter,too-many-instance-attributes,too-many-branches, no-member, bad-option-value, arguments-renamedimportitertoolsimportnumpyasnpimportpennylaneasqmlfrompennylane.measurementsimportMeasurementProcessfrompennylane.wiresimportWiresfrom._qubit_deviceimportQubitDevice
[docs]classQutritDevice(QubitDevice):# pylint: disable=too-many-public-methods"""Abstract base class for PennyLane qutrit devices. The following abstract method **must** be defined: * :meth:`~.apply`: append circuit operations, compile the circuit (if applicable), and perform the quantum computation. Devices that generate their own samples (such as hardware) may optionally overwrite :meth:`~.probability`. This method otherwise automatically computes the probabilities from the generated samples, and **must** overwrite the following method: * :meth:`~.generate_samples`: Generate samples from the device from the exact or approximate probability distribution. Analytic devices **must** overwrite the following method: * :meth:`~.analytic_probability`: returns the probability or marginal probability from the device after circuit execution. :meth:`~.marginal_prob` may be used here. This device contains common utility methods for qutrit-based devices. These do not need to be overwritten. Utility methods include: * :meth:`~.expval`, :meth:`~.var`, :meth:`~.sample`: return expectation values, variances, and samples of observables after the circuit has been rotated into the observable eigenbasis. Args: wires (int, Iterable[Number, str]]): Number of subsystems represented by the device, or iterable that contains unique labels for the subsystems as numbers (i.e., ``[-1, 0, 2]``) or strings (``['ancilla', 'q1', 'q2']``). Default 1 if not specified. shots (None, int, list[int]): Number of circuit evaluations/random samples used to estimate expectation values of observables. If ``None``, the device calculates probability, expectation values, and variances analytically. If an integer, it specifies the number of samples to estimate these quantities. If a list of integers is passed, the circuit evaluations are batched over the list of shots. r_dtype: Real floating point precision type. c_dtype: Complex floating point precision type. """# TODO: Update set of supported observables as new observables are addedobservables={"Identity","THermitian",}
[docs]defgenerate_samples(self):r"""Returns the computational basis samples generated for all wires. Note that PennyLane uses the convention :math:`|q_0,q_1,\dots,q_{N-1}\rangle` where :math:`q_0` is the most significant trit. .. warning:: This method should be overwritten on devices that generate their own computational basis samples, with the resulting computational basis samples stored as ``self._samples``. Returns: array[complex]: array of samples in the shape ``(dev.shots, dev.num_wires)`` """number_of_states=3**self.num_wiresrotated_prob=self.analytic_probability()samples=self.sample_basis_states(number_of_states,rotated_prob)returnself.states_to_ternary(samples,self.num_wires)
[docs]defgenerate_basis_states(self,num_wires,dtype=np.uint32):"""Generates basis states in ternary representation according to the number of wires specified. Args: num_wires (int): the number of wires dtype=np.uint32 (type): the data type of the arrays to use Returns: array[int]: the sampled basis states """basis_states_generator=itertools.product((0,1,2),repeat=num_wires)returnnp.fromiter(itertools.chain(*basis_states_generator),dtype=dtype).reshape(-1,num_wires)
[docs]@staticmethoddefstates_to_ternary(samples,num_wires,dtype=np.int64):"""Convert basis states from base 10 to ternary representation. This is an auxiliary method to the generate_samples method. Args: samples (array[int]): samples of basis states in base 10 representation num_wires (int): the number of qutrits dtype (type): Type of the internal integer array to be used. Can be important to specify for large systems for memory allocation purposes. Returns: array[int]: basis states in ternary representation """ternary_arr=[]forsampleinsamples:num=[]for_inrange(num_wires):sample,r=divmod(sample,3)num.append(r)ternary_arr.append(num[::-1])returnnp.array(ternary_arr,dtype=dtype)
[docs]defdensity_matrix(self,wires):"""Returns the reduced density matrix prior to measurement. Args: wires (Wires): wires of the reduced system Raises: QuantumFunctionError: density matrix is currently unsupported on :class:`~.QutritDevice` """# TODO: Add support for DensityMatrix return type. Currently, qml.math is hard coded to calculate this for qubit# states (see `qml.math.reduced_dm()`), so it needs to be updated before DensityMatrix can be supported for qutrits.# For now, if a user tries to request this return type, an error will be raised.raiseqml.QuantumFunctionError("Unsupported return type specified for observable density matrix")
[docs]defvn_entropy(self,wires,log_base):r"""Returns the Von Neumann entropy prior to measurement. .. math:: S( \rho ) = -\text{Tr}( \rho \log ( \rho )) Args: wires (Wires): Wires of the considered subsystem. log_base (float): Base for the logarithm, default is None the natural logarithm is used in this case. Raises: QuantumFunctionError: Von Neumann entropy is currently unsupported on :class:`~.QutritDevice` """# TODO: Add support for VnEntropy return type. Currently, qml.math is hard coded to calculate this for qubit# states (see `qml.math.vn_entropy()`), so it needs to be updated before VnEntropy can be supported for qutrits.# For now, if a user tries to request this return type, an error will be raised.raiseqml.QuantumFunctionError("Unsupported return type specified for observable Von Neumann entropy")
[docs]defmutual_info(self,wires0,wires1,log_base):r"""Returns the mutual information prior to measurement: .. math:: I(A, B) = S(\rho^A) + S(\rho^B) - S(\rho^{AB}) where :math:`S` is the von Neumann entropy. Args: wires0 (Wires): wires of the first subsystem wires1 (Wires): wires of the second subsystem log_base (float): base to use in the logarithm Raises: QuantumFunctionError: Mutual information is currently unsupported on :class:`~.QutritDevice` """# TODO: Add support for MutualInfo return type. Currently, qml.math is hard coded to calculate this for qubit# states (see `qml.math.mutual_info()`), so it needs to be updated before MutualInfo can be supported for qutrits.# For now, if a user tries to request this return type, an error will be raised.raiseqml.QuantumFunctionError("Unsupported return type specified for observable mutual information")
[docs]defclassical_shadow(self,obs,circuit):""" Returns the measured trits and recipes in the classical shadow protocol. Please refer to :func:`~.pennylane.measurements.classical_shadow` for detailed documentation. .. seealso:: :func:`~pennylane.measurements.classical_shadow` Args: obs (~.pennylane.measurements.ClassicalShadowMP): The classical shadow measurement process circuit (~.tapes.QuantumTape): The quantum tape that is being executed Raises: QuantumFunctionError: Classical shadow is currently unsupported on :class:`~.QutritDevice` """# TODO: Add support for ClassicalShadowMPraiseqml.QuantumFunctionError("Qutrit devices don't support classical shadow measurements.")
[docs]defshadow_expval(self,obs,circuit):r"""Compute expectation values using classical shadows in a differentiable manner. Please refer to :func:`~pennylane.shadow_expval` for detailed documentation. .. seealso:: :func:`~pennylane.shadow_expval` Args: obs (~.pennylane.measurements.ShadowExpvalMP): The classical shadow expectation value measurement process circuit (~.tapes.QuantumTape): The quantum tape that is being executed Raises: QuantumFunctionError: Shadow Expectation values are currently unsupported on :class:`~.QutritDevice` """# TODO: Add support for ShadowExpvalMPraiseqml.QuantumFunctionError("Qutrit devices don't support shadow expectation value measurements.")
[docs]defestimate_probability(self,wires=None,shot_range=None,bin_size=None):"""Return the estimated probability of each computational basis state using the generated samples. Args: wires (Iterable[Number, str], Number, str, Wires): wires to calculate marginal probabilities for. Wires not provided are traced out of the system. shot_range (tuple[int]): 2-tuple of integers specifying the range of samples to use. If not specified, all samples are used. bin_size (int): Divides the shot range into bins of size ``bin_size``, and returns the measurement statistic separately over each bin. If not provided, the entire shot range is treated as a single bin. Returns: array[float]: list of the probabilities """wires=wiresorself.wires# convert to a wires objectwires=Wires(wires)# translate to wire labels used by devicedevice_wires=self.map_wires(wires)sample_slice=Ellipsisifshot_rangeisNoneelseslice(*shot_range)samples=self._samples[sample_slice,device_wires]# convert samples from a list of 0, 1, 2 integers, to base 10 representationpowers_of_three=3**np.arange(len(device_wires))[::-1]indices=samples@powers_of_three# count the basis state occurrences, and construct the probability vectorifbin_sizeisnotNone:bins=len(samples)//bin_sizeindices=indices.reshape((bins,-1))prob=np.zeros([3**len(device_wires),bins],dtype=np.float64)forb,idxinenumerate(indices):basis_states,counts=np.unique(idx,return_counts=True)prob[basis_states,b]=counts/bin_sizeelse:basis_states,counts=np.unique(indices,return_counts=True)prob=np.zeros([3**len(device_wires)],dtype=np.float64)prob[basis_states]=counts/len(samples)returnself._asarray(prob,dtype=self.R_DTYPE)
[docs]defmarginal_prob(self,prob,wires=None):r"""Return the marginal probability of the computational basis states by summing the probabiliites on the non-specified wires. If no wires are specified, then all the basis states representable by the device are considered and no marginalization takes place. .. note:: If the provided wires are not in the order as they appear on the device, the returned marginal probabilities take this permutation into account. For example, if the addressable wires on this device are ``Wires([0, 1, 2])`` and this function gets passed ``wires=[2, 0]``, then the returned marginal probability vector will take this 'reversal' of the two wires into account: .. math:: \mathbb{P}^{(2, 0)} = \left[ |00\rangle, |10\rangle, |20\rangle, |01\rangle, |11\rangle, |21\rangle, |02\rangle, |12\rangle, |22\rangle \right] Args: prob: The probabilities to return the marginal probabilities for wires (Iterable[Number, str], Number, str, Wires): wires to return marginal probabilities for. Wires not provided are traced out of the system. Returns: array[float]: array of the resulting marginal probabilities. """ifwiresisNone:# no need to marginalizereturnprobwires=Wires(wires)# determine which subsystems are to be summed overinactive_wires=Wires.unique_wires([self.wires,wires])# translate to wire labels used by devicedevice_wires=self.map_wires(wires)inactive_device_wires=self.map_wires(inactive_wires)# reshape the probability so that each axis corresponds to a wireprob=self._reshape(prob,[3]*self.num_wires)# sum over all inactive wires# hotfix to catch when default.qutrit uses this method# since then device_wires is a listifisinstance(inactive_device_wires,Wires):wires=inactive_device_wires.labelselse:wires=inactive_device_wiresprob=self._reduce_sum(prob,wires)prob=self._transpose(prob,np.argsort(np.argsort(device_wires)))returnself._flatten(prob)
[docs]defsample(self,observable,shot_range=None,bin_size=None,counts=False):def_samples_to_counts(samples,no_observable_provided):"""Group the obtained samples into a dictionary. **Example** >>> samples tensor([[0, 0, 1], [0, 0, 1], [1, 1, 1]], requires_grad=True) >>> self._samples_to_counts(samples) {'111':1, '001':2} """ifno_observable_provided:# If we describe a state vector, we need to convert its list representation# into string (it's hashable and good-looking).# Before converting to str, we need to extract elements from arrays# to satisfy the case of jax interface, as jax arrays do not support str.samples=["".join([str(s.item())forsinsample])forsampleinsamples]states,counts=np.unique(samples,return_counts=True)returndict(zip(states,counts))# TODO: Add special cases for any observables that require them once list of# observables is updated.# translate to wire labels used by devicedevice_wires=self.map_wires(observable.wires)sample_slice=Ellipsisifshot_rangeisNoneelseslice(*shot_range)no_observable_provided=isinstance(observable,MeasurementProcess)ifno_observable_provided:# if no observable was provided then return the raw samplesif(len(observable.wires)!=0):# if wires are provided, then we only return samples from those wiressamples=self._samples[sample_slice,np.array(device_wires)]else:samples=self._samples[sample_slice]else:# Replace the basis state in the computational basis with the correct eigenvalue.# Extract only the columns of the basis samples required based on ``wires``.samples=self._samples[sample_slice,np.array(device_wires)]# Add np.array here for Jax support.powers_of_three=3**np.arange(samples.shape[-1])[::-1]indices=samples@powers_of_threeindices=np.array(indices)# Add np.array here for Jax support.try:samples=observable.eigvals()[indices]exceptqml.operation.EigvalsUndefinedErrorase:# if observable has no info on eigenvalues, we cannot return this measurementraiseqml.operation.EigvalsUndefinedError(f"Cannot compute samples of {observable.name}.")fromeifbin_sizeisNone:ifcounts:return_samples_to_counts(samples,no_observable_provided)returnsamplesnum_wires=len(device_wires)iflen(device_wires)>0elseself.num_wiresifcounts:shape=(-1,bin_size,num_wires)ifno_observable_providedelse(-1,bin_size)return[_samples_to_counts(bin_sample,no_observable_provided)forbin_sampleinsamples.reshape(shape)]return(samples.reshape((num_wires,bin_size,-1))ifno_observable_providedelsesamples.reshape((bin_size,-1)))
# TODO: Implement function. Currently unimplemented due to lack of decompositions available# for existing operations and lack of non-parametrized observables.