# Copyright 2018-2021 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:`QubitDevice` abstract base class."""# For now, arguments may be different from the signatures provided in Device# 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-renamed# pylint: disable=too-many-argumentsimportabcimportinspectimportitertoolsimportloggingimportwarningsfromcollectionsimportdefaultdictfromtypingimportUnionimportnumpyasnpimportpennylaneasqmlfrompennylane.mathimportmultiplyasqmlmulfrompennylane.mathimportsumasqmlsumfrompennylane.measurementsimport(ClassicalShadowMP,CountsMP,ExpectationMP,MeasurementProcess,MeasurementTransform,MeasurementValue,MidMeasureMP,MutualInfoMP,ProbabilityMP,SampleMeasurement,SampleMP,ShadowExpvalMP,Shots,StateMeasurement,StateMP,VarianceMP,VnEntropyMP,)frompennylane.operationimportOperation,operation_derivativefrompennylane.resourceimportResourcesfrompennylane.tapeimportQuantumScriptfrompennylane.wiresimportWiresfrom._legacy_deviceimportDevicelogger=logging.getLogger(__name__)logger.addHandler(logging.NullHandler())def_sample_to_str(sample):"""Converts a bit-array to a string. For example, ``[0, 1]`` would become '01'."""return"".join(map(str,sample))
[docs]classQubitDevice(Device):"""Abstract base class for PennyLane qubit 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 qubit-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. """# pylint: disable=too-many-public-methods_asarray=staticmethod(np.asarray)_dot=staticmethod(np.dot)_abs=staticmethod(np.abs)_reduce_sum=staticmethod(lambdaarray,axes:np.sum(array,axis=tuple(axes)))_reshape=staticmethod(np.reshape)_flatten=staticmethod(lambdaarray:array.flatten())_gather=staticmethod(lambdaarray,indices,axis=0:array[:,indices]ifaxis==1elsearray[indices])# Make sure to only use _gather with axis=0 or axis=1_einsum=staticmethod(np.einsum)_cast=staticmethod(np.asarray)_transpose=staticmethod(np.transpose)_tensordot=staticmethod(np.tensordot)_conj=staticmethod(np.conj)_imag=staticmethod(np.imag)_roll=staticmethod(np.roll)_stack=staticmethod(np.stack)_outer=staticmethod(np.outer)_diag=staticmethod(np.diag)_real=staticmethod(np.real)_size=staticmethod(np.size)_ndim=staticmethod(np.ndim)@staticmethoddef_scatter(indices,array,new_dimensions):new_array=np.zeros(new_dimensions,dtype=array.dtype.type)new_array[indices]=arrayreturnnew_array@staticmethoddef_const_mul(constant,array):"""Data type preserving multiply operation"""returnqmlmul(constant,array,dtype=array.dtype)observables={"PauliX","PauliY","PauliZ","Hadamard","Hermitian","Identity","Projector","Sum","SProd","Prod",}measurement_map=defaultdict(lambda:"")# e.g. {SampleMP: "sample"}"""Mapping used to override the logic of measurement processes. The dictionary maps a measurement class to a string containing the name of a device's method that overrides the measurement process. The method defined by the device should have the following arguments: * measurement (MeasurementProcess): measurement to override * 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. .. note:: When overriding the logic of a :class:`~pennylane.measurements.MeasurementTransform`, the method defined by the device should only have a single argument: * tape: quantum tape to transform """def__init__(self,wires=1,shots=None,*,r_dtype=np.float64,c_dtype=np.complex128,analytic=None):super().__init__(wires=wires,shots=shots,analytic=analytic)if"float"notinstr(r_dtype):raiseqml.DeviceError("Real datatype must be a floating point type.")if"complex"notinstr(c_dtype):raiseqml.DeviceError("Complex datatype must be a complex floating point type.")self.C_DTYPE=c_dtypeself.R_DTYPE=r_dtypeself._samples=None"""None or array[int]: stores the samples generated by the device *after* rotation to diagonalize the observables."""
[docs]defreset(self):"""Reset the backend state. After the reset, the backend should be as if it was just constructed. Most importantly the quantum state is reset to its initial value. """self._samples=None
[docs]defexecute(self,circuit,**kwargs):"""It executes a queue of quantum operations on the device and then measure the given observables. For plugin developers: instead of overwriting this, consider implementing a suitable subset of * :meth:`apply` * :meth:`~.generate_samples` * :meth:`~.probability` Additional keyword arguments may be passed to this method that can be utilised by :meth:`apply`. An example would be passing the ``QNode`` hash that can be used later for parametric compilation. Args: circuit (~.tape.QuantumTape): circuit to execute on the device Raises: QuantumFunctionError: if the observable is not supported Returns: array[float]: measured value(s) """iflogger.isEnabledFor(logging.DEBUG):logger.debug("Entry with args=(circuit=%s, kwargs=%s) called by=%s",circuit,kwargs,"::L".join(str(i)foriininspect.getouterframes(inspect.currentframe(),2)[1][1:3]),)self.check_validity(circuit.operations,circuit.observables)has_mcm=any(isinstance(op,MidMeasureMP)foropincircuit.operations)ifhas_mcmand"mid_measurements"notinkwargs:results=[]aux_circ=circuit.copy(shots=[1])# Some devices like Lightning-Kokkos use `self.shots` to update `_samples`,# and hence we update `self.shots` temporarily for this loopshots_copy=self.shotsself.shots=1for_incircuit.shots:kwargs["mid_measurements"]={}self.reset()results.append(self.execute(aux_circ,**kwargs))self.shots=shots_copyreturntuple(results)# apply all circuit operationsself.apply(circuit.operations,rotations=self._get_diagonalizing_gates(circuit),**kwargs,)ifhas_mcm:mid_measurements=kwargs["mid_measurements"]# generate computational basis samplessample_type=(SampleMP,CountsMP,ClassicalShadowMP,ShadowExpvalMP)ifself.shotsisnotNoneorany(isinstance(m,sample_type)formincircuit.measurements):# Lightning does not support apply(rotations) anymore, so we need to rotate here# Lightning without binaries fallbacks to `QubitDevice`, and hence the _CPP_BINARY_AVAILABLE conditionis_lightning=self._is_lightning_device()diagonalizing_gates=self._get_diagonalizing_gates(circuit)ifis_lightningelseNoneifis_lightninganddiagonalizing_gates:# pragma: no coverself.apply(diagonalizing_gates)self._samples=self.generate_samples()ifis_lightninganddiagonalizing_gates:# pragma: no cover# pylint: disable=bad-reversed-sequenceself.apply([qml.adjoint(g,lazy=False)forginreversed(diagonalizing_gates)])# compute the required statisticsifhas_mcm:n_mcms=len(mid_measurements)stat_circuit=qml.tape.QuantumScript(circuit.operations,circuit.measurements[0:-n_mcms],shots=1,trainable_params=circuit.trainable_params,)else:stat_circuit=circuitifself._shot_vectorisnotNone:results=self.shot_vec_statistics(stat_circuit)else:results=self.statistics(stat_circuit)ifhas_mcm:results.extend(list(mid_measurements.values()))single_measurement=len(circuit.measurements)==1results=results[0]ifsingle_measurementelsetuple(results)# increment counter for number of executions of qubit deviceself._num_executions+=1ifself.tracker.active:shots_from_dev=self._shotsifnotself.shot_vectorelseself._raw_shot_sequencetape_resources=circuit.specs["resources"]resources=Resources(# temporary until shots get updated on tape !tape_resources.num_wires,tape_resources.num_gates,tape_resources.gate_types,tape_resources.gate_sizes,tape_resources.depth,Shots(shots_from_dev),)self.tracker.update(executions=1,shots=self._shots,results=results,resources=resources)self.tracker.record()returnresults
[docs]defshot_vec_statistics(self,circuit:QuantumScript):"""Process measurement results from circuit execution using a device with a shot vector and return statistics. This is an auxiliary method of execute and uses statistics. When using shot vectors, measurement results for each item of the shot vector are contained in a tuple. Args: circuit (~.tape.QuantumTape): circuit to execute on the device Raises: QuantumFunctionError: if the observable is not supported Returns: tuple: statistics for each shot item from the shot vector """results=[]s1=0measurements=circuit.measurementscounts_exist=any(isinstance(m,CountsMP)forminmeasurements)single_measurement=len(measurements)==1forshot_tupleinself._shot_vector:s2=s1+np.prod(shot_tuple)r=self.statistics(circuit,shot_range=[s1,s2],bin_size=shot_tuple.shots)# This will likely be required:# if qml.math.get_interface(*r) == "jax": # pylint: disable=protected-access# r = r[0]ifsingle_measurement:r=r[0]elifshot_tuple.copies==1:r=tuple(r_[0]ifisinstance(r_,list)elser_.Tforr_inr)elifcounts_exist:r=self._multi_meas_with_counts_shot_vec(circuit,shot_tuple,r)else:# r is a nested sequence, contains the results for# multiple measurements## Each item of r has copies length, we need to extract# each measurement result from the arrays# 1. transpose: applied because measurements like probs# for multiple copies output results with shape (N,# copies) and we'd like to index straight to get rows# which requires a shape of (copies, N)# 2. asarray: done because indexing into a flat array produces a# scalar instead of a scalar shaped arrayr=[tuple(self._asarray(r_.T[idx])forr_inr)foridxinrange(shot_tuple.copies)]ifisinstance(r,qml.numpy.ndarray):ifshot_tuple.copies>1:results.extend([self._asarray(r_)forr_inqml.math.unstack(r.T)])else:results.append(r.T)elifsingle_measurementandcounts_exist:# Results are nested in a sequenceresults.extend(r)elifnotsingle_measurementandshot_tuple.copies>1:# Some samples may still be transposed, fix their shapes# Leave dictionaries intactr=[tuple(elemifisinstance(elem,dict)elseelem.Tforeleminr_)forr_inr]results.extend(r)else:results.append(r)s1=s2returntuple(results)
def_multi_meas_with_counts_shot_vec(self,circuit:QuantumScript,shot_tuple,r):"""Auxiliary function of the shot_vec_statistics and execute functions for post-processing the results of multiple measurements at least one of which was a counts measurement. The measurements were executed on a device that defines a shot vector. """# First: iterate over each group of measurement# results that contain copies many outcomes for a# single measurementnew_r=[]# Each item of r has copies lengthforidxinrange(shot_tuple.copies):result_group=[]foridx2,r_inenumerate(r):measurement_proc=circuit.measurements[idx2]ifisinstance(measurement_proc,ProbabilityMP)or(isinstance(measurement_proc,SampleMP)andmeasurement_proc.obs):# Here, the result has a shape of (num_basis_states, shot_tuple.copies)# Extract a single row -> shape (num_basis_states,)result=r_[:,idx]else:result=r_[idx]ifnotisinstance(measurement_proc,CountsMP):result=self._asarray(result.T)result_group.append(result)new_r.append(tuple(result_group))returnnew_r
[docs]defbatch_execute(self,circuits,**kwargs):"""Execute a batch of quantum circuits on the device. The circuits are represented by tapes, and they are executed one-by-one using the device's ``execute`` method. The results are collected in a list. For plugin developers: This function should be overwritten if the device can efficiently run multiple circuits on a backend, for example using parallel and/or asynchronous executions. Args: circuits (list[~.tape.QuantumTape]): circuits to execute on the device Returns: list[array[float]]: list of measured value(s) """iflogger.isEnabledFor(logging.DEBUG):logger.debug("""Entry with args=(circuits=%s) called by=%s""",circuits,"::L".join(str(i)foriininspect.getouterframes(inspect.currentframe(),2)[1][1:3]),)ifself.capabilities().get("supports_mid_measure",False):kwargs.setdefault("postselect_mode",None)results=[]forcircuitincircuits:# we need to reset the device here, else it will# not start the next computation in the zero stateself.reset()res=self.execute(circuit,**kwargs)results.append(res)ifself.tracker.active:self.tracker.update(batches=1,batch_len=len(circuits))self.tracker.record()returnresults
[docs]@abc.abstractmethoddefapply(self,operations,**kwargs):"""Apply quantum operations, rotate the circuit into the measurement basis, and compile and execute the quantum circuit. This method receives a list of quantum operations queued by the QNode, and should be responsible for: * Constructing the quantum program * (Optional) Rotating the quantum circuit using the rotation operations provided. This diagonalizes the circuit so that arbitrary observables can be measured in the computational basis. * Compile the circuit * Execute the quantum circuit Both arguments are provided as lists of PennyLane :class:`~.Operation` instances. Useful properties include :attr:`~.Operation.name`, :attr:`~.Operation.wires`, and :attr:`~.Operation.parameters`: >>> op = qml.RX(0.2, wires=[0]) >>> op.name # returns the operation name "RX" >>> op.wires # returns a Wires object representing the wires that the operation acts on Wires([0]) >>> op.parameters # returns a list of parameters [0.2] Args: operations (list[~.Operation]): operations to apply to the device Keyword args: rotations (list[~.Operation]): operations that rotate the circuit pre-measurement into the eigenbasis of the observables. hash (int): the hash value of the circuit constructed by `CircuitGraph.hash` """
[docs]@staticmethoddefactive_wires(operators):"""Returns the wires acted on by a set of operators. Args: operators (list[~.Operation]): operators for which we are gathering the active wires Returns: Wires: wires activated by the specified operators """list_of_wires=[op.wiresforopinoperators]returnWires.all_wires(list_of_wires)
def_measure(self,measurement:Union[SampleMeasurement,StateMeasurement],shot_range=None,bin_size=None,):"""Compute the corresponding measurement process depending on ``shots`` and the measurement type. Args: measurement (Union[SampleMeasurement, StateMeasurement]): measurement process 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. Raises: ValueError: if the measurement cannot be computed Returns: Union[float, dict, list[float]]: result of the measurement """ifself.shotsisNone:ifisinstance(measurement,StateMeasurement):returnmeasurement.process_state(state=self.state,wire_order=self.wires)raiseValueError("Shots must be specified in the device to compute the measurement "f"{measurement.__class__.__name__}")ifisinstance(measurement,StateMeasurement):warnings.warn(f"Requested measurement {measurement.__class__.__name__} with finite shots; the ""returned state information is analytic and is unaffected by sampling. ""To silence this warning, set shots=None on the device.",UserWarning,)returnmeasurement.process_state(state=self.state,wire_order=self.wires)returnmeasurement.process_samples(samples=self._samples,wire_order=self.wires,shot_range=shot_range,bin_size=bin_size)
[docs]defstatistics(self,circuit:QuantumScript,shot_range=None,bin_size=None):# pylint: disable=too-many-statements"""Process measurement results from circuit execution and return statistics. This includes returning expectation values, variance, samples, probabilities, states, and density matrices. Args: circuit (~.tape.QuantumTape): the quantum tape currently being executed 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. Raises: QuantumFunctionError: if a measurement is not supported Returns: Union[float, List[float]]: the corresponding statistics .. details:: :title: Usage Details The ``shot_range`` and ``bin_size`` arguments allow for the statistics to be performed on only a subset of device samples. This finer level of control is accessible from the main UI by instantiating a device with a batch of shots. For example, consider the following device: >>> dev = qml.device("my_device", shots=[5, (10, 3), 100]) This device will execute QNodes using 135 shots, however measurement statistics will be **course grained** across these 135 shots: * All measurement statistics will first be computed using the first 5 shots --- that is, ``shots_range=[0, 5]``, ``bin_size=5``. * Next, the tuple ``(10, 3)`` indicates 10 shots, repeated 3 times. We will want to use ``shot_range=[5, 35]``, performing the expectation value in bins of size 10 (``bin_size=10``). * Finally, we repeat the measurement statistics for the final 100 shots, ``shot_range=[35, 135]``, ``bin_size=100``. """measurements=circuit.measurementsresults=[]forminmeasurements:# TODO: Remove this when all overridden measurements support the `MeasurementProcess` classifisinstance(m.mv,list):# MeasurementProcess stores information needed for processing if terminal measurement# uses a list of mid-circuit measurement valuesobs=m# pragma: no coverelse:obs=m.obsorm.mvobs=mifobsisNoneelseobs# Check if there is an overridden version of the measurement processifmethod:=getattr(self,self.measurement_map[type(m)],False):ifisinstance(m,MeasurementTransform):result=method(tape=circuit)else:result=method(m,shot_range=shot_range,bin_size=bin_size)# 1. Based on the measurement type, compute statistics# Pass instances directlyelifisinstance(m,ExpectationMP):result=self.expval(obs,shot_range=shot_range,bin_size=bin_size)elifisinstance(m,VarianceMP):result=self.var(obs,shot_range=shot_range,bin_size=bin_size)elifisinstance(m,SampleMP):samples=self.sample(obs,shot_range=shot_range,bin_size=bin_size,counts=False)dtype=intifisinstance(obs,SampleMP)elseNoneresult=self._asarray(qml.math.squeeze(samples),dtype=dtype)elifisinstance(m,CountsMP):result=self.sample(m,shot_range=shot_range,bin_size=bin_size,counts=True)elifisinstance(m,ProbabilityMP):is_lightning=self._is_lightning_device()diagonalizing_gates=(self._get_diagonalizing_gates(circuit)ifis_lightningelseNone)ifis_lightninganddiagonalizing_gates:# pragma: no coverself.apply(diagonalizing_gates)result=self.probability(wires=m.wires,shot_range=shot_range,bin_size=bin_size)ifis_lightninganddiagonalizing_gates:# pragma: no cover# pylint: disable=bad-reversed-sequenceself.apply([qml.adjoint(g,lazy=False)forginreversed(diagonalizing_gates)])elifisinstance(m,StateMP):iflen(measurements)>1:raiseqml.QuantumFunctionError("The state or density matrix cannot be returned in combination ""with other return types")ifself.shotsisnotNone:warnings.warn("Requested state or density matrix with finite shots; the returned ""state information is analytic and is unaffected by sampling. To silence ""this warning, set shots=None on the device.",UserWarning,)# Check if the state is accessible and decide to return the state or the density# matrix.state=self.access_state(wires=obs.wires)result=self._asarray(state,dtype=self.C_DTYPE)elifisinstance(m,VnEntropyMP):ifself.wires.labels!=tuple(range(self.num_wires)):raiseqml.QuantumFunctionError("Returning the Von Neumann entropy is not supported when using custom wire labels")ifself._shot_vectorisnotNone:raiseNotImplementedError("Returning the Von Neumann entropy is not supported with shot vectors.")ifself.shotsisnotNone:warnings.warn("Requested Von Neumann entropy with finite shots; the returned ""result is analytic and is unaffected by sampling. To silence ""this warning, set shots=None on the device.",UserWarning,)result=self.vn_entropy(wires=obs.wires,log_base=obs.log_base)elifisinstance(m,MutualInfoMP):ifself.wires.labels!=tuple(range(self.num_wires)):raiseqml.QuantumFunctionError("Returning the mutual information is not supported when using custom wire labels")ifself._shot_vectorisnotNone:raiseNotImplementedError("Returning the mutual information is not supported with shot vectors.")ifself.shotsisnotNone:warnings.warn("Requested mutual information with finite shots; the returned ""state information is analytic and is unaffected by sampling. To silence ""this warning, set shots=None on the device.",UserWarning,)wires0,wires1=obs.raw_wiresresult=self.mutual_info(wires0=wires0,wires1=wires1,log_base=obs.log_base)elifisinstance(m,ClassicalShadowMP):iflen(measurements)>1:raiseqml.QuantumFunctionError("Classical shadows cannot be returned in combination ""with other return types")result=self.classical_shadow(obs,circuit)elifisinstance(m,ShadowExpvalMP):iflen(measurements)>1:raiseqml.QuantumFunctionError("Classical shadows cannot be returned in combination ""with other return types")result=self.shadow_expval(obs,circuit=circuit)elifisinstance(m,MeasurementTransform):result=m.process(tape=circuit,device=self)elifisinstance(m,(SampleMeasurement,StateMeasurement)):result=self._measure(m,shot_range=shot_range,bin_size=bin_size)else:name=obs.nameifisinstance(obs,qml.operation.Operator)elsetype(obs).__name__raiseqml.QuantumFunctionError(f"Unsupported return type specified for observable {name}")# 2. Post-process statistics results (if need be)ifisinstance(m,(ExpectationMP,VarianceMP,ProbabilityMP,VnEntropyMP,MutualInfoMP,ShadowExpvalMP,),):# Result is a floatresult=self._asarray(result,dtype=self.R_DTYPE)ifself._shot_vectorisnotNoneandisinstance(result,np.ndarray):# In the shot vector case, measurement results may be of shape (N, 1) instead of (N,)# Squeeze the result to transform the results## E.g.,# before:# [[0.489]# [0.511]# [0. ]# [0. ]]## after: [0.489 0.511 0. 0. ]result=qml.math.squeeze(result)# 3. Append to final listifresultisnotNone:results.append(result)returnresults
[docs]defaccess_state(self,wires=None):"""Check that the device has access to an internal state and return it if available. Args: wires (Wires): wires of the reduced system Raises: QuantumFunctionError: if the device is not capable of returning the state Returns: array or tensor: the state or the density matrix of the device """ifnotself.capabilities().get("returns_state"):raiseqml.QuantumFunctionError("The current device is not capable of returning the state")state=getattr(self,"state",None)ifstateisNone:raiseqml.QuantumFunctionError("The state is not available in the current device")ifwires:density_matrix=self.density_matrix(wires)returndensity_matrixreturnstate
[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 bit. .. 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=2**self.num_wiresrotated_prob=self.analytic_probability()samples=self.sample_basis_states(number_of_states,rotated_prob)returnself.states_to_binary(samples,self.num_wires)
[docs]defsample_basis_states(self,number_of_states,state_probability):"""Sample from the computational basis states based on the state probability. This is an auxiliary method to the generate_samples method. Args: number_of_states (int): the number of basis states to sample from state_probability (array[float]): the computational basis probability vector Returns: array[int]: the sampled basis states """ifself.shotsisNone:raiseqml.QuantumFunctionError("The number of shots has to be explicitly set on the device ""when using sample-based measurements.")shots=self.shotsbasis_states=np.arange(number_of_states)# pylint:disable = import-outside-toplevelif(qml.math.is_abstract(state_probability)andqml.math.get_interface(state_probability)=="jax"):importjaxkey=jax.random.PRNGKey(np.random.randint(0,2**31))ifjax.numpy.ndim(state_probability)==2:returnjax.numpy.array([jax.random.choice(key,basis_states,shape=(shots,),p=prob)forprobinstate_probability])returnjax.random.choice(key,basis_states,shape=(shots,),p=state_probability)state_probs=qml.math.unwrap(state_probability)ifself._ndim(state_probability)==2:# np.random.choice does not support broadcasting as needed here.returnnp.array([np.random.choice(basis_states,shots,p=prob)forprobinstate_probs])returnnp.random.choice(basis_states,shots,p=state_probs)
[docs]@staticmethoddefgenerate_basis_states(num_wires,dtype=np.uint32):""" Generates basis states in binary representation according to the number of wires specified. The states_to_binary method creates basis states faster (for larger systems at times over x25 times faster) than the approach using ``itertools.product``, at the expense of using slightly more memory. Due to the large size of the integer arrays for more than 32 bits, memory allocation errors may arise in the states_to_binary method. Hence we constraint the dtype of the array to represent unsigned integers on 32 bits. Due to this constraint, an overflow occurs for 32 or more wires, therefore this approach is used only for fewer wires. For smaller number of wires speed is comparable to the next approach (using ``itertools.product``), hence we resort to that one for testing purposes. Args: num_wires (int): the number wires dtype=np.uint32 (type): the data type of the arrays to use Returns: array[int]: the sampled basis states """if2<num_wires<32:states_base_ten=np.arange(2**num_wires,dtype=dtype)returnQubitDevice.states_to_binary(states_base_ten,num_wires,dtype=dtype)# A slower, but less memory intensive methodbasis_states_generator=itertools.product((0,1),repeat=num_wires)# pragma: no coverreturnnp.fromiter(itertools.chain(*basis_states_generator),dtype=int).reshape(-1,num_wires)
[docs]@staticmethoddefstates_to_binary(samples,num_wires,dtype=np.int64):"""Convert basis states from base 10 to binary 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 qubits 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 binary representation """powers_of_two=1<<np.arange(num_wires,dtype=dtype)# `samples` typically is one-dimensional, but can be two-dimensional with broadcasting.# In any case we want to append a new axis at the *end* of the shape.states_sampled_base_ten=samples[...,None]&powers_of_two# `states_sampled_base_ten` can be two- or three-dimensional. We revert the *last* axis.return(states_sampled_base_ten>0).astype(dtype)[...,::-1]
@propertydefcircuit_hash(self):"""The hash of the circuit upon the last execution. This can be used by devices in :meth:`~.apply` for parametric compilation. """raiseNotImplementedError@propertydefstate(self):"""Returns the state vector of the circuit prior to measurement. .. note:: Only state vector simulators support this property. Please see the plugin documentation for more details. """raiseNotImplementedError
[docs]defdensity_matrix(self,wires):"""Returns the reduced density matrix over the given wires. Args: wires (Wires): wires of the reduced system Returns: array[complex]: complex array of shape ``(2 ** len(wires), 2 ** len(wires))`` representing the reduced density matrix of the state prior to measurement. """state=getattr(self,"state",None)wires=self.map_wires(wires)returnqml.math.reduce_statevector(state,indices=wires,c_dtype=self.C_DTYPE)
[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. Returns: float: returns the Von Neumann entropy """try:state=self.density_matrix(wires=self.wires)except(qml.QuantumFunctionError,NotImplementedError)ase:# pragma: no coverraiseNotImplementedError(f"Cannot compute the Von Neumman entropy with device {self.name} that is not capable of returning the "f"state. ")fromewires=wires.tolist()returnqml.math.vn_entropy(state,indices=wires,c_dtype=self.C_DTYPE,base=log_base)
[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 Returns: float: the mutual information """try:state=self.density_matrix(wires=self.wires)except(qml.QuantumFunctionError,NotImplementedError)ase:# pragma: no coverraiseNotImplementedError(f"Cannot compute the mutual information with device {self.name} that is not capable of returning the "f"state. ")fromewires0=wires0.tolist()wires1=wires1.tolist()returnqml.math.mutual_info(state,indices0=wires0,indices1=wires1,c_dtype=self.C_DTYPE,base=log_base)
[docs]defclassical_shadow(self,obs,circuit):""" Returns the measured bits and recipes in the classical shadow protocol. The protocol is described in detail in the `classical shadows paper <https://arxiv.org/abs/2002.08953>`_. This measurement process returns the randomized Pauli measurements (the ``recipes``) that are performed for each qubit and snapshot as an integer: - 0 for Pauli X, - 1 for Pauli Y, and - 2 for Pauli Z. It also returns the measurement results (the ``bits``); 0 if the 1 eigenvalue is sampled, and 1 if the -1 eigenvalue is sampled. The device shots are used to specify the number of snapshots. If ``T`` is the number of shots and ``n`` is the number of qubits, then both the measured bits and the Pauli measurements have shape ``(T, n)``. This implementation is device-agnostic and works by executing single-shot tapes containing randomized Pauli observables. Devices should override this if they can offer cleaner or faster implementations. .. seealso:: :func:`~.pennylane.classical_shadow` Args: obs (~.pennylane.measurements.ClassicalShadowMP): The classical shadow measurement process circuit (~.tape.QuantumTape): The quantum tape that is being executed Returns: tensor_like[int]: A tensor with shape ``(2, T, n)``, where the first row represents the measured bits and the second represents the recipes used. """ifcircuitisNone:# pragma: no coverraiseValueError("Circuit must be provided when measuring classical shadows")wires=obs.wiresn_snapshots=self.shotsseed=obs.seedoriginal_shots=self.shotsoriginal_shot_vector=self._shot_vectortry:self.shots=1# slow implementation but works for all devicesn_qubits=len(wires)mapped_wires=np.array(self.map_wires(wires))# seed the random measurement generation so that recipes# are the same for different executions with the same seedrng=np.random.RandomState(seed)recipes=rng.randint(0,3,size=(n_snapshots,n_qubits))obs_list=[qml.X,qml.Y,qml.Z]outcomes=np.zeros((n_snapshots,n_qubits))fortinrange(n_snapshots):# compute rotations for the Pauli measurementsrotations=[rotforwire_idx,wireinenumerate(wires)forrotinobs_list[recipes[t][wire_idx]].compute_diagonalizing_gates(wires=wire)]self.reset()self.apply(circuit.operations,rotations=self._get_diagonalizing_gates(circuit)+rotations)outcomes[t]=self.generate_samples()[0][mapped_wires]finally:self.shots=original_shots# pylint: disable=attribute-defined-outside-initself._shot_vector=original_shot_vectorreturnself._cast(self._stack([outcomes,recipes]),dtype=np.int8)
[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. Args: obs (~.pennylane.measurements.ClassicalShadowMP): The classical shadow expectation value measurement process circuit (~.tape.QuantumTape): The quantum tape that is being executed Returns: float: expectation value estimate. """bits,recipes=self.classical_shadow(obs,circuit)shadow=qml.shadows.ClassicalShadow(bits,recipes,wire_map=obs.wires.tolist())returnshadow.expval(obs.H,obs.k)
[docs]defanalytic_probability(self,wires=None):r"""Return the (marginal) probability of each computational basis state from the last run of the device. PennyLane uses the convention :math:`|q_0,q_1,\dots,q_{N-1}\rangle` where :math:`q_0` is the most significant bit. If no wires are specified, then all the basis states representable by the device are considered and no marginalization takes place. .. note:: :meth:`~.marginal_prob` may be used as a utility method to calculate the marginal probability distribution. Args: 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]: list of the probabilities """raiseNotImplementedError
[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)num_wires=len(device_wires)ifshot_rangeisNone:# The Ellipsis (...) corresponds to broadcasting and shots dimensions or only shotssamples=self._samples[...,device_wires]else:# The Ellipsis (...) corresponds to the broadcasting dimension or no axis at allsamples=self._samples[...,slice(*shot_range),device_wires]# convert samples from a list of 0, 1 integers, to base 10 representationpowers_of_two=2**np.arange(num_wires)[::-1]indices=samples@powers_of_two# `self._samples` typically has two axes ((shots, wires)) but can also have three with# broadcasting ((batch_size, shots, wires)) so that we simply read out the batch_size.batch_size=self._samples.shape[0]ifnp.ndim(self._samples)==3elseNonedim=2**num_wires# count the basis state occurrences, and construct the probability vectorifbin_sizeisnotNone:num_bins=samples.shape[-2]//bin_sizeprob=self._count_binned_samples(indices,batch_size,dim,bin_size,num_bins)else:prob=self._count_unbinned_samples(indices,batch_size,dim)returnself._asarray(prob,dtype=self.R_DTYPE)
@staticmethoddef_count_unbinned_samples(indices,batch_size,dim):"""Count the occurences of sampled indices and convert them to relative counts in order to estimate their occurence probability."""ifbatch_sizeisNone:prob=np.zeros(dim,dtype=np.float64)basis_states,counts=np.unique(indices,return_counts=True)prob[basis_states]=counts/len(indices)returnprobprob=np.zeros((batch_size,dim),dtype=np.float64)fori,idxinenumerate(indices):# iterate over the broadcasting dimensionbasis_states,counts=np.unique(idx,return_counts=True)prob[i,basis_states]=counts/len(idx)returnprob@staticmethoddef_count_binned_samples(indices,batch_size,dim,bin_size,num_bins):"""Count the occurences of bins of sampled indices and convert them to relative counts in order to estimate their occurence probability per bin."""ifbatch_sizeisNone:prob=np.zeros((dim,num_bins),dtype=np.float64)indices=indices.reshape((num_bins,bin_size))# count the basis state occurrences, and construct the probability vector for each binforb,idxinenumerate(indices):basis_states,counts=np.unique(idx,return_counts=True)prob[basis_states,b]=counts/bin_sizereturnprobprob=np.zeros((batch_size,dim,num_bins),dtype=np.float64)indices=indices.reshape((batch_size,num_bins,bin_size))# count the basis state occurrences, and construct the probability vector# for each bin and broadcasting indexfori,_indicesinenumerate(indices):# First iterate over broadcasting dimensionforb,idxinenumerate(_indices):# Then iterate over bins dimensionbasis_states,counts=np.unique(idx,return_counts=True)prob[i,basis_states,b]=counts/bin_sizereturnprob
[docs]defprobability(self,wires=None,shot_range=None,bin_size=None):"""Return either the analytic probability or estimated probability of each computational basis state. Devices that require a finite number of shots always return the estimated probability. Args: 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]: list of the probabilities """wires=wiresorself.wiresifself.shotsisNone:returnself.analytic_probability(wires=wires)returnself.estimate_probability(wires=wires,shot_range=shot_range,bin_size=bin_size)
@staticmethoddef_get_batch_size(tensor,expected_shape,expected_size):"""Determine whether a tensor has an additional batch dimension for broadcasting, compared to an expected_shape. As QubitDevice does not natively support broadcasting, it always reports no batch size, that is ``batch_size=None``"""# pylint: disable=unused-argumentreturnNone
[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, |01\rangle, |11\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. """dim=2**self.num_wiresbatch_size=self._get_batch_size(prob,(dim,),dim)# pylint: disable=assignment-from-noneifwiresisNone:# 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)# hotfix to catch when default.qubit uses this method# since then device_wires is a listifisinstance(inactive_device_wires,Wires):inactive_device_wires=inactive_device_wires.labels# reshape the probability so that each axis corresponds to a wireshape=[2]*self.num_wiresdesired_axes=np.argsort(np.argsort(device_wires))flat_shape=(-1,)ifbatch_sizeisnotNone:# prob now is reshaped to have self.num_wires+1 axes in the case of broadcastingshape.insert(0,batch_size)inactive_device_wires=[idx+1foridxininactive_device_wires]desired_axes=np.insert(desired_axes+1,0,0)flat_shape=(batch_size,-1)prob=self._reshape(prob,shape)# sum over all inactive wiresprob=self._reduce_sum(prob,inactive_device_wires)# rearrange wires to desired orderprob=self._transpose(prob,desired_axes)# flatten and return probabilitiesreturnself._reshape(prob,flat_shape)
[docs]defexpval(self,observable,shot_range=None,bin_size=None):# exact expectation valueifself.shotsisNone:try:eigvals=self._asarray((observable.eigvals()ifnotisinstance(observable,MeasurementValue)# Indexing a MeasurementValue gives the output of the processing function# for that index as a binary number.else[observable[i]foriinrange(2**len(observable.measurements))]),dtype=self.R_DTYPE,)exceptqml.operation.EigvalsUndefinedErrorase:raiseqml.operation.EigvalsUndefinedError(f"Cannot compute analytic expectations of {observable.name}.")fromeprob=self.probability(wires=observable.wires)# In case of broadcasting, `prob` has two axes and this is a matrix-vector productreturnself._dot(prob,eigvals)# estimate the evsamples=self.sample(observable,shot_range=shot_range,bin_size=bin_size)# With broadcasting, we want to take the mean over axis 1, which is the -1st/-2nd with/# without bin_size. Without broadcasting, axis 0 is the -1st/-2nd with/without bin_sizeaxis=-1ifbin_sizeisNoneelse-2# TODO: do we need to squeeze here? Maybe remove with new return typesreturnnp.squeeze(np.mean(samples,axis=axis))
[docs]defvar(self,observable,shot_range=None,bin_size=None):# exact variance valueifself.shotsisNone:try:eigvals=self._asarray((observable.eigvals()ifnotisinstance(observable,MeasurementValue)# Indexing a MeasurementValue gives the output of the processing function# for that index as a binary number.else[observable[i]foriinrange(2**len(observable.measurements))]),dtype=self.R_DTYPE,)exceptqml.operation.EigvalsUndefinedErrorase:# if observable has no info on eigenvalues, we cannot return this measurementraiseqml.operation.EigvalsUndefinedError(f"Cannot compute analytic variance of {observable.name}.")fromeprob=self.probability(wires=observable.wires)# In case of broadcasting, `prob` has two axes and these are a matrix-vector productsreturnself._dot(prob,(eigvals**2))-self._dot(prob,eigvals)**2# estimate the variancesamples=self.sample(observable,shot_range=shot_range,bin_size=bin_size)# With broadcasting, we want to take the variance over axis 1, which is the -1st/-2nd with/# without bin_size. Without broadcasting, axis 0 is the -1st/-2nd with/without bin_sizeaxis=-1ifbin_sizeisNoneelse-2# TODO: do we need to squeeze here? Maybe remove with new return typesreturnnp.squeeze(np.var(samples,axis=axis))
def_samples_to_counts(self,samples,mp:CountsMP,num_wires):"""Groups the samples into a dictionary showing number of occurences for each possible outcome. The format of the dictionary depends on whether the MeasurementProcess CountsMP returns all counts or not. By default, the dictionary will only contain the observed outcomes. Optionally (all_outcomes=True) the dictionary will instead contain all possible outcomes, with a count of 0 for those not observed. See example. Args: samples: An array of samples, with the shape being ``(shots,len(wires))`` if no observable is provided, with sample values being an array of 0s or 1s for each wire. Otherwise, it has shape ``(shots,)``, with sample values being scalar eigenvalues of the observable mp (~.measurements.CountsMP): the measurement process sampled num_wires (int): number of wires the sampled observable was performed on Returns: dict: dictionary with format ``{'outcome': num_occurences}``, including all outcomes for the sampled observable **Example** >>> from pennylane import numpy as np >>> num_wires = 2 >>> dev = qml.device("default.mixed", wires=num_wires) >>> mp = qml.counts() >>> samples = np.array([[0, 0], [0, 0], [1, 0]]) >>> dev._samples_to_counts(samples, mp, num_wires) {'00': 2, '10': 1} >>> mp = qml.counts(all_outcomes=True) >>> dev._samples_to_counts(samples, mp, num_wires) {'00': 2, '01': 0, '10': 1, '11': 0} The variable all_outcomes can be set when running measurements.counts, i.e.: .. code-block:: python3 dev = qml.device("default.qubit", wires=2, shots=4) @qml.qnode(dev) def circuit(x): qml.RX(x, wires=0) return qml.counts(all_outcomes=True) """outcomes=[]# if an observable was provided, batched samples will have shape (batch_size, shots)batched_ndims=2shape=samples.shapeifmp.obsisNoneandnotisinstance(mp.mv,MeasurementValue):# convert samples and outcomes (if using) from arrays to str for dict keyssamples=np.array([sampleforsampleinsamplesifnotnp.any(np.isnan(sample))])samples=qml.math.cast_like(samples,qml.math.int8(0))samples=np.apply_along_axis(_sample_to_str,-1,samples)batched_ndims=3# no observable was provided, batched samples will have shape (batch_size, shots, len(wires))ifmp.all_outcomes:outcomes=list(map(_sample_to_str,self.generate_basis_states(num_wires)))elifmp.all_outcomes:outcomes=mp.eigvals()batched=len(shape)==batched_ndimsifnotbatched:samples=samples[None]# generate empty outcome dict, populate values with state countsbase_dict={k:np.int64(0)forkinoutcomes}outcome_dicts=[base_dict.copy()for_inrange(shape[0])]results=[np.unique(batch,return_counts=True)forbatchinsamples]forresult,outcome_dictinzip(results,outcome_dicts):states,counts=resultforstate,countinzip(states,counts):outcome_dict[state]=countreturnoutcome_dictsifbatchedelseoutcome_dicts[0]
[docs]defsample(self,observable,shot_range=None,bin_size=None,counts=False):"""Return samples of an observable. Args: observable (Observable): the observable to sample 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. counts (bool): whether counts (``True``) or raw samples (``False``) should be returned Raises: EigvalsUndefinedError: if no information is available about the eigenvalues of the observable Returns: Union[array[float], dict, list[dict]]: samples in an array of dimension ``(shots,)`` or counts """mp=observableno_observable_provided=Falseifisinstance(mp,MeasurementProcess):ifmp.obsisnotNone:observable=mp.obselifmp.mvisnotNoneandisinstance(mp.mv,MeasurementValue):observable=mp.mv# pragma: no coverelse:no_observable_provided=True# translate to wire labels used by device. observable is list when measuring sequence# of multiple MeasurementValuesdevice_wires=self.map_wires(observable.wires)# Select the samples from self._samples that correspond to ``shot_range`` if providedifshot_rangeisNone:sub_samples=self._sampleselse:# Indexing corresponds to: (potential broadcasting, shots, wires). Note that the last# colon (:) is required because shots is the second-to-last axis and the# Ellipsis (...) otherwise would take up broadcasting and shots axes.sub_samples=self._samples[...,slice(*shot_range),:]ifno_observable_provided:# if no observable was provided then return the raw samplesiflen(observable.wires)!=0:# if wires are provided, then we only return samples from those wiressamples=sub_samples[...,np.array(device_wires)]else:samples=sub_sampleselse:# get eigvalsifisinstance(observable,MeasurementValue):eigvals=self._asarray(# pragma: no cover[observable[i]foriinrange(2**len(observable.measurements))],dtype=self.R_DTYPE,)else:try:eigvals=observable.eigvals()exceptqml.operation.EigvalsUndefinedErrorase:# if observable has no info on eigenvalues, we cannot return this measurementraiseqml.operation.EigvalsUndefinedError(f"Cannot compute samples of {observable.name}.")frome# special handling for observables with eigvals +1/-1 to enable JIT compatibility.ifnp.array_equal(eigvals,[1.0,-1.0]):samples=1.0-2*sub_samples[...,device_wires[0]]# type should be floatelse:# Replace the basis state in the computational basis with the correct eigenvalue.# Extract only the columns of the basis samples required based on ``wires``.# Add np.array here for Jax support.samples=sub_samples[...,np.array(device_wires)]powers_of_two=2**np.arange(samples.shape[-1])[::-1]indices=samples@powers_of_twoindices=np.array(indices)# Add np.array here for Jax support.samples=eigvals[indices]num_wires=len(device_wires)iflen(device_wires)>0elseself.num_wiresifbin_sizeisNone:ifcounts:returnself._samples_to_counts(samples,mp,num_wires)returnsamplesifcounts:shape=(-1,bin_size,num_wires)ifno_observable_providedelse(-1,bin_size)return[self._samples_to_counts(bin_sample,mp,num_wires)forbin_sampleinsamples.reshape(shape)]return(samples.T.reshape((num_wires,bin_size,-1))ifno_observable_providedelsesamples.reshape((bin_size,-1)))
[docs]defadjoint_jacobian(self,tape:QuantumScript,starting_state=None,use_device_state=False):# pylint: disable=too-many-statements"""Implements the adjoint method outlined in `Jones and Gacon <https://arxiv.org/abs/2009.02823>`__ to differentiate an input tape. After a forward pass, the circuit is reversed by iteratively applying adjoint gates to scan backwards through the circuit. .. note:: The adjoint differentiation method has the following restrictions: * As it requires knowledge of the statevector, only statevector simulator devices can be used. * Only expectation values are supported as measurements. * Cannot differentiate with respect to state-prep operations. * Does not work for parametrized observables like :class:`~.ops.LinearCombination` or :class:`~.Hermitian`. Args: tape (.QuantumTape): circuit that the function takes the gradient of Keyword Args: starting_state (tensor_like): post-forward pass state to start execution with. It should be complex-valued. Takes precedence over ``use_device_state``. use_device_state (bool): use current device state to initialize. A forward pass of the same circuit should be the last thing the device has executed. If a ``starting_state`` is provided, that takes precedence. Returns: array or tuple[array]: the derivative of the tape with respect to trainable parameters. Dimensions are ``(len(observables), len(trainable_params))``. Raises: QuantumFunctionError: if the input tape has measurements that are not expectation values or contains a multi-parameter operation aside from :class:`~.Rot` """iftape.batch_sizeisnotNone:raiseqml.QuantumFunctionError("Parameter broadcasting is not supported with adjoint differentiation")# broadcasted inner product not summing over first dimension of bsum_axes=tuple(range(1,self.num_wires+1))# pylint: disable=unnecessary-lambda-assignmentdot_product_real=lambdab,k:self._real(qmlsum(self._conj(b)*k,axis=sum_axes))formintape.measurements:ifnotisinstance(m,ExpectationMP):raiseqml.QuantumFunctionError("Adjoint differentiation method does not support"f" measurement {m.__class__.__name__}")ifnotm.obs.has_matrix:raiseqml.QuantumFunctionError("Adjoint differentiation method does not support Hamiltonian observables.")ifself.shot_vectorisnotNone:raiseqml.QuantumFunctionError("Adjoint does not support shot vectors.")ifself.shotsisnotNone:warnings.warn("Requested adjoint differentiation to be computed with finite shots. ""The derivative is always exact when using the adjoint differentiation method.",UserWarning,)# Initialization of stateifstarting_stateisnotNone:ket=self._reshape(starting_state,[2]*self.num_wires)else:ifnotuse_device_state:self.reset()self.execute(tape)ket=self._pre_rotated_staten_obs=len(tape.observables)bras=np.empty([n_obs]+[2]*self.num_wires,dtype=np.complex128)forkkinrange(n_obs):bras[kk,...]=self._apply_operation(ket,tape.observables[kk])expanded_ops=[]foropinreversed(tape.operations):ifop.num_params>1:ifnotisinstance(op,qml.Rot):raiseqml.QuantumFunctionError(f"The {op.name} operation is not supported using "'the "adjoint" differentiation method')ops=op.decomposition()expanded_ops.extend(reversed(ops))elifop.namenotin("StatePrep","BasisState","Snapshot"):expanded_ops.append(op)trainable_params=[]forkintape.trainable_params:# pylint: disable=protected-accessmp_or_op=tape[tape.par_info[k]["op_idx"]]ifisinstance(mp_or_op,MeasurementProcess):warnings.warn("Differentiating with respect to the input parameters of "f"{mp_or_op.obs.name} is not supported with the ""adjoint differentiation method. Gradients are computed ""only with regards to the trainable parameters of the circuit.\n\n Mark ""the parameters of the measured observables as non-trainable ""to silence this warning.",UserWarning,)else:trainable_params.append(k)jac=np.zeros((len(tape.observables),len(trainable_params)))param_number=len(tape.get_parameters(trainable_only=False,operations_only=True))-1trainable_param_number=len(trainable_params)-1foropinexpanded_ops:adj_op=qml.adjoint(op)ket=self._apply_operation(ket,adj_op)ifop.num_params==1:ifparam_numberintrainable_params:d_op_matrix=operation_derivative(op)ket_temp=self._apply_unitary(ket,d_op_matrix,op.wires)jac[:,trainable_param_number]=2*dot_product_real(bras,ket_temp)trainable_param_number-=1param_number-=1forkkinrange(n_obs):bras[kk,...]=self._apply_operation(bras[kk,...],adj_op)returnself._adjoint_jacobian_processing(jac)
@staticmethoddef_adjoint_jacobian_processing(jac):""" Post-process the Jacobian matrix returned by ``adjoint_jacobian`` for the new return type system. """jac=np.squeeze(jac)ifjac.ndim==0:returnnp.array(jac)ifjac.ndim==1:returntuple(np.array(j)forjinjac)# must be 2-dimensionalreturntuple(tuple(np.array(j_)forj_inj)forjinjac)def_get_diagonalizing_gates(self,circuit:QuantumScript)->list[Operation]:"""Returns the gates that diagonalize the measured wires such that they are in the eigenbasis of the circuit observables. Note that this exists as a method of the Device class to enable child classes to override the implementation if necessary (for example, to skip computing rotation gates for a measurement that doesn't need them). Args: circuit (~.tape.QuantumTape): The circuit containing observables that may need diagonalizing Returns: List[~.Operation]: the operations that diagonalize the observables """# pylint:disable=no-self-usereturncircuit.diagonalizing_gatesdef_is_lightning_device(self):"""Returns True if the device is a Lightning plugin with C++ binaries and False otherwise."""return(hasattr(self,"name")and"Lightning"instr(self.name)andgetattr(self,"_CPP_BINARY_AVAILABLE",False))