# 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 defines the QuantumScript object responsible for storing quantum operations and measurements to beexecuted by a device."""# pylint: disable=too-many-instance-attributes, protected-access, too-many-public-methodsimportcontextlibimportcopyfromcollectionsimportCounterfromcollections.abcimportCallable,Hashable,Iterable,Iterator,Sequencefromfunctoolsimportcached_propertyfromtypingimportAny,Optional,TypeVar,Unionimportpennylaneasqmlfrompennylane.measurementsimportMeasurementProcessfrompennylane.measurements.shotsimportShots,ShotsLikefrompennylane.operationimport_UNSET_BATCH_SIZE,Observable,Operation,Operatorfrompennylane.pytreesimportregister_pytreefrompennylane.queuingimportAnnotatedQueue,process_queuefrompennylane.typingimportTensorLikefrompennylane.wiresimportWires,WiresLikeOPENQASM_GATES={"CNOT":"cx","CZ":"cz","U3":"u3","U2":"u2","U1":"u1","Identity":"id","PauliX":"x","PauliY":"y","PauliZ":"z","Hadamard":"h","S":"s","Adjoint(S)":"sdg","T":"t","Adjoint(T)":"tdg","RX":"rx","RY":"ry","RZ":"rz","CRX":"crx","CRY":"cry","CRZ":"crz","SWAP":"swap","Toffoli":"ccx","CSWAP":"cswap","PhaseShift":"u1",}"""dict[str, str]: Maps PennyLane gate names to equivalent QASM gate names.Note that QASM has two native gates:- ``U`` (equivalent to :class:`~.U3`)- ``CX`` (equivalent to :class:`~.CNOT`)All other gates are defined in the file stdgates.inc:https://github.com/Qiskit/openqasm/blob/master/examples/stdgates.inc"""QS=TypeVar("QS",bound="QuantumScript")classSpecsDict(dict):"""A special dictionary for storing the specs of a circuit. Used to customize ``KeyError`` messages."""def__getitem__(self,__k):if__k=="num_diagonalizing_gates":raiseKeyError("num_diagonalizing_gates is no longer in specs due to the ambiguity of the definition ""and extreme performance costs.")try:returnsuper().__getitem__(__k)exceptKeyErrorase:raiseKeyError(f"key {__k} not available. Options are {set(self.keys())}")frome
[docs]classQuantumScript:r"""The operations and measurements that represent instructions for execution on a quantum device. Args: ops (Iterable[Operator]): An iterable of the operations to be performed measurements (Iterable[MeasurementProcess]): All the measurements to be performed Keyword Args: shots (None, int, Sequence[int], ~.Shots): Number and/or batches of shots for execution. Note that this property is still experimental and under development. trainable_params (None, Sequence[int]): the indices for which parameters are trainable .. seealso:: :class:`pennylane.tape.QuantumTape` **Example:** .. code-block:: python from pennylane.tape import QuantumScript ops = [qml.BasisState(np.array([1,1]), wires=(0,"a")), qml.RX(0.432, 0), qml.RY(0.543, 0), qml.CNOT((0,"a")), qml.RX(0.133, "a")] qscript = QuantumScript(ops, [qml.expval(qml.Z(0))]) >>> list(qscript) [BasisState(array([1, 1]), wires=[0, "a"]), RX(0.432, wires=[0]), RY(0.543, wires=[0]), CNOT(wires=[0, 'a']), RX(0.133, wires=['a']), expval(Z(0))] >>> qscript.operations [BasisState(array([1, 1]), wires=[0, "a"]), RX(0.432, wires=[0]), RY(0.543, wires=[0]), CNOT(wires=[0, 'a']), RX(0.133, wires=['a'])] >>> qscript.measurements [expval(Z(0))] Iterating over the quantum script can be done by: >>> for op in qscript: ... print(op) BasisState(array([1, 1]), wires=[0, "a"]) RX(0.432, wires=[0]) RY(0.543, wires=[0]) CNOT(wires=[0, 'a']) RX(0.133, wires=['a']) expval(Z(0))' Quantum scripts also support indexing and length determination: >>> qscript[0] BasisState(array([1, 1]), wires=[0, "a"]) >>> len(qscript) 6 Once constructed, the script can be executed directly on a quantum device using the :func:`~.pennylane.execute` function: >>> dev = qml.device('default.qubit', wires=(0,'a')) >>> qml.execute([qscript], dev, diff_method=None) [array([-0.77750694])] Quantum scripts can also store information about the number and batches of executions by setting the ``shots`` keyword argument. This information is internally stored in a :class:`pennylane.measurements.Shots` object: >>> s_vec = [1, 1, 2, 2, 2] >>> qscript = QuantumScript([qml.Hadamard(0)], [qml.expval(qml.Z(0))], shots=s_vec) >>> qscript.shots.shot_vector (ShotCopies(1 shots x 2), ShotCopies(2 shots x 3)) ``ops`` and ``measurements`` are converted to lists upon initialization, so those arguments accept any iterable object: >>> qscript = QuantumScript((qml.X(i) for i in range(3))) >>> qscript.circuit [X(0), X(1), X(2)] """def_flatten(self):return(self._ops,self.measurements),(self.shots,tuple(self.trainable_params))@classmethoddef_unflatten(cls,data,metadata):returncls(*data,shots=metadata[0],trainable_params=metadata[1])def__init__(self,ops:Optional[Iterable[Operator]]=None,measurements:Optional[Iterable[MeasurementProcess]]=None,shots:Optional[ShotsLike]=None,trainable_params:Optional[Sequence[int]]=None,):self._ops=[]ifopsisNoneelselist(ops)self._measurements=[]ifmeasurementsisNoneelselist(measurements)self._shots=Shots(shots)self._trainable_params=trainable_paramsself._graph=Noneself._specs=Noneself._batch_size=_UNSET_BATCH_SIZEself._obs_sharing_wires=Noneself._obs_sharing_wires_id=Nonedef__repr__(self)->str:returnf"<{self.__class__.__name__}: wires={self.wires.tolist()}, params={self.num_params}>"@cached_propertydefhash(self)->int:"""int: returns an integer hash uniquely representing the quantum script"""fingerprint=[]fingerprint.extend(op.hashforopinself.operations)fingerprint.extend(m.hashforminself.measurements)fingerprint.extend(self.trainable_params)fingerprint.extend(self.shots)returnhash(tuple(fingerprint))def__iter__(self)->Iterator[Union[Operator,MeasurementProcess]]:"""Iterator[.Operator, .MeasurementProcess]: Return an iterator to the underlying quantum circuit object."""returniter(self.circuit)def__getitem__(self,idx:int)->Union[Operator,MeasurementProcess]:"""Union[Operator, MeasurementProcess]: Return the indexed operator from underlying quantum circuit object."""returnself.circuit[idx]def__len__(self)->int:"""int: Return the number of operations and measurements in the underlying quantum circuit object."""returnlen(self.circuit)# ========================================================# QSCRIPT properties# ========================================================@propertydefcircuit(self)->list[Union[Operator,MeasurementProcess]]:"""Returns the underlying quantum circuit as a list of operations and measurements. The circuit is created with the assumptions that: * The ``operations`` attribute contains quantum operations and mid-circuit measurements and * The ``measurements`` attribute contains terminal measurements. Note that the resulting list could contain MeasurementProcess objects that some devices may not support. Returns: list[.Operator, .MeasurementProcess]: the quantum circuit containing quantum operations and measurements """returnself.operations+self.measurements@propertydefoperations(self)->list[Operator]:"""Returns the state preparations and operations on the quantum script. Returns: list[.Operator]: quantum operations >>> ops = [qml.StatePrep([0, 1], 0), qml.RX(0.432, 0)] >>> qscript = QuantumScript(ops, [qml.expval(qml.Z(0))]) >>> qscript.operations [StatePrep([0, 1], wires=[0]), RX(0.432, wires=[0])] """returnself._ops@propertydefobservables(self)->list[Union[MeasurementProcess,Observable]]:"""Returns the observables on the quantum script. Returns: list[.MeasurementProcess, .Observable]]: list of observables **Example** >>> ops = [qml.StatePrep([0, 1], 0), qml.RX(0.432, 0)] >>> qscript = QuantumScript(ops, [qml.expval(qml.Z(0))]) >>> qscript.observables [expval(Z(0))] """# TODO: modify this property once devices# have been refactored to accept and understand recieving# measurement processes rather than specific observables.obs=[]forminself.measurements:ifm.obsisnotNone:obs.append(m.obs)else:obs.append(m)returnobs@propertydefmeasurements(self)->list[MeasurementProcess]:"""Returns the measurements on the quantum script. Returns: list[.MeasurementProcess]: list of measurement processes **Example** >>> ops = [qml.StatePrep([0, 1], 0), qml.RX(0.432, 0)] >>> qscript = QuantumScript(ops, [qml.expval(qml.Z(0))]) >>> qscript.measurements [expval(Z(0))] """returnself._measurements@propertydefsamples_computational_basis(self)->bool:"""Determines if any of the measurements are in the computational basis."""returnany(o.samples_computational_basisforoinself.measurements)@propertydefnum_params(self)->int:"""Returns the number of trainable parameters on the quantum script."""returnlen(self.trainable_params)@propertydefbatch_size(self)->Optional[int]:r"""The batch size of the quantum script inferred from the batch sizes of the used operations for parameter broadcasting. .. seealso:: :attr:`~.Operator.batch_size` for details. Returns: int or None: The batch size of the quantum script if present, else ``None``. """ifself._batch_sizeis_UNSET_BATCH_SIZE:self._update_batch_size()returnself._batch_size@propertydefdiagonalizing_gates(self)->list[Operation]:"""Returns the gates that diagonalize the measured wires such that they are in the eigenbasis of the circuit observables. Returns: List[~.Operation]: the operations that diagonalize the observables **Examples** For a tape with a single observable, we get the diagonalizing gate of that observable: >>> tape = qml.tape.QuantumScript([], [qml.expval(X(0))]) >>> tape.diagonalizing_gates [H(0)] If the tape includes multiple observables, they are each diagonalized individually: >>> tape = qml.tape.QuantumScript([], [qml.expval(X(0)), qml.var(Y(1))]) >>> tape.diagonalizing_gates [H(0), Z(1), S(1), H(1)] .. warning:: If the tape contains multiple observables acting on the same wire, then ``tape.diagonalizing_gates`` will include multiple conflicting diagonalizations. For example: >>> tape = qml.tape.QuantumScript([], [qml.expval(X(0)), qml.var(Y(0))]) >>> tape.diagonalizing_gates [H(0), Z(0), S(0), H(0)] If it is relevant for your application, applying :func:`~.pennylane.transforms.split_non_commuting` to a tape will split it into multiple tapes with only qubit-wise commuting observables. Generally, composite operators are handled by diagonalizing their component parts, for example: >>> tape = qml.tape.QuantumScript([], [qml.expval(X(0)+Y(1))]) >>> tape.diagonalizing_gates [H(0), Z(1), S(1), H(1)] However, for operators that contain multiple terms on the same wire, a single diagonalizing operator will be returned that diagonalizes the full operator as a unit: >>> tape = qml.tape.QuantumScript([], [qml.expval(X(0)+Y(0))]) >>> tape.diagonalizing_gates [QubitUnitary(array([[-0.70710678-0.j , 0.5 -0.5j], [-0.70710678-0.j , -0.5 +0.5j]]), wires=[0])] """rotation_gates=[]withqml.queuing.QueuingManager.stop_recording():forobservablein_get_base_obs(self.observables):# some observables do not have diagonalizing gates,# in which case we just don't append anywithcontextlib.suppress(qml.operation.DiagGatesUndefinedError):rotation_gates.extend(observable.diagonalizing_gates())returnrotation_gates@propertydefshots(self)->Shots:"""Returns a ``Shots`` object containing information about the number and batches of shots Returns: ~.Shots: Object with shot information """returnself._shots@propertydefnum_preps(self)->int:"""Returns the index of the first operator that is not an StatePrepBase operator."""idx=0num_ops=len(self.operations)whileidx<num_opsandisinstance(self.operations[idx],qml.operation.StatePrepBase):idx+=1returnidx@propertydefop_wires(self)->Wires:"""Returns the wires that the tape operations act on."""returnWires.all_wires(op.wiresforopinself.operations)##### Update METHODS ###############def_update(self):"""Update all internal metadata regarding processed operations and observables"""self._graph=Noneself._specs=Noneself._trainable_params=Nonetry:# Invalidate cached properties so they get recalculateddelself.wiresdelself.par_infodelself.hashexceptAttributeError:pass@cached_propertydefwires(self)->Wires:"""Returns the wires used in the quantum script process Returns: ~.Wires: wires in quantum script process """returnWires.all_wires(dict.fromkeys(op.wiresforopinself))@propertydefnum_wires(self)->int:"""Returns the number of wires in the quantum script process Returns: int: number of wires in quantum script process """returnlen(self.wires)@cached_propertydefpar_info(self)->list[dict[str,Union[int,Operator]]]:"""Returns the parameter information of the operations and measurements in the quantum script. Returns: list[dict[str, Operator or int]]: list of dictionaries with parameter information. **Example** >>> ops = [qml.StatePrep([0, 1], 0), qml.RX(0.432, 0), qml.CNOT((0,1))] >>> qscript = QuantumScript(ops, [qml.expval(qml.Z(0))]) >>> qscript.par_info [{'op': StatePrep(array([0, 1]), wires=[0]), 'op_idx': 0, 'p_idx': 0}, {'op': RX(0.432, wires=[0]), 'op_idx': 1, 'p_idx': 0}] Note that the operations and measurements included in this list are only the ones that have parameters """par_info=[]foridx,opinenumerate(self.operations):par_info.extend({"op":op,"op_idx":idx,"p_idx":i}fori,dinenumerate(op.data))n_ops=len(self.operations)foridx,minenumerate(self.measurements):ifm.obsisnotNone:par_info.extend({"op":m.obs,"op_idx":idx+n_ops,"p_idx":i}fori,dinenumerate(m.obs.data))returnpar_info@propertydefobs_sharing_wires(self)->list[Operator]:"""Returns the subset of the observables that share wires with another observable, i.e., that do not have their own unique set of wires. Returns: list[~.Observable]: list of observables with shared wires. """ifself._obs_sharing_wiresisNone:self._update_observables()returnself._obs_sharing_wires@propertydefobs_sharing_wires_id(self)->list[int]:"""Returns the indices subset of the observables that share wires with another observable, i.e., that do not have their own unique set of wires. Returns: list[int]: list of indices from observables with shared wires. """ifself._obs_sharing_wires_idisNone:self._update_observables()returnself._obs_sharing_wires_iddef_update_observables(self):"""Update information about observables, including the wires that are acted upon and identifying any observables that share wires. Sets: _obs_sharing_wires (list[~.Observable]): Observables that share wires with any other observable _obs_sharing_wires_id (list[int]): Indices of the measurements that contain the observables in _obs_sharing_wires """obs_wires=[wireforminself.measurementsforwireinm.wiresifm.obsisnotNone]self._obs_sharing_wires=[]self._obs_sharing_wires_id=[]iflen(obs_wires)!=len(set(obs_wires)):c=Counter(obs_wires)repeated_wires={wforwinobs_wiresifc[w]>1}fori,minenumerate(self.measurements):ifm.obsisnotNoneandlen(set(m.wires)&repeated_wires)>0:self._obs_sharing_wires.append(m.obs)self._obs_sharing_wires_id.append(i)def_update_batch_size(self):"""Infer the batch_size of the quantum script from the batch sizes of its operations and check the latter for consistency. Sets: _batch_size (int): The common batch size of the quantum script operations, if any has one """candidate=Noneforopinself.operations:op_batch_size=getattr(op,"batch_size",None)ifop_batch_sizeisNone:continueifcandidate:ifop_batch_size!=candidate:raiseValueError("The batch sizes of the quantum script operations do not match, they include "f"{candidate} and {op_batch_size}.")else:candidate=op_batch_sizeself._batch_size=candidate# ========================================================# Parameter handling# ========================================================@propertydefdata(self)->list[TensorLike]:"""Alias to :meth:`~.get_parameters` and :meth:`~.set_parameters` for backwards compatibilities with operations."""returnself.get_parameters(trainable_only=False)@propertydeftrainable_params(self)->list[int]:r"""Store or return a list containing the indices of parameters that support differentiability. The indices provided match the order of appearance in the quantum circuit. Setting this property can help reduce the number of quantum evaluations needed to compute the Jacobian; parameters not marked as trainable will be automatically excluded from the Jacobian computation. The number of trainable parameters determines the number of parameters passed to :meth:`~.set_parameters`, and changes the default output size of method :meth:`~.get_parameters()`. .. note:: For devices that support native backpropagation (such as ``default.qubit`` and ``default.mixed``), this property contains no relevant information when using backpropagation to compute gradients. **Example** >>> ops = [qml.RX(0.432, 0), qml.RY(0.543, 0), ... qml.CNOT((0,"a")), qml.RX(0.133, "a")] >>> qscript = QuantumScript(ops, [qml.expval(qml.Z(0))]) >>> qscript.trainable_params [0, 1, 2] >>> qscript.trainable_params = [0] # set only the first parameter as trainable >>> qscript.get_parameters() [0.432] """ifself._trainable_paramsisNone:self._trainable_params=list(range(len(self.par_info)))returnself._trainable_params@trainable_params.setterdeftrainable_params(self,param_indices:list[int]):"""Store the indices of parameters that support differentiability. Args: param_indices (list[int]): parameter indices """ifany(notisinstance(i,int)ori<0foriinparam_indices):raiseValueError("Argument indices must be non-negative integers.")num_params=len(self.par_info)ifany(i>num_paramsforiinparam_indices):raiseValueError(f"Quantum Script only has {num_params} parameters.")self._trainable_params=sorted(set(param_indices))
[docs]defget_operation(self,idx:int)->tuple[Operator,int,int]:"""Returns the trainable operation, the operation index and the corresponding operation argument index, for a specified trainable parameter index. Args: idx (int): the trainable parameter index Returns: tuple[.Operation, int, int]: tuple containing the corresponding operation, operation index and an integer representing the argument index, for the provided trainable parameter. """# get the index of the parameter in the scriptt_idx=self.trainable_params[idx]# get the info for the parameterinfo=self.par_info[t_idx]returninfo["op"],info["op_idx"],info["p_idx"]
[docs]defget_parameters(self,trainable_only:bool=True,operations_only:bool=False,**kwargs,# pylint:disable=unused-argument)->list[TensorLike]:"""Return the parameters incident on the quantum script operations. The returned parameters are provided in order of appearance on the quantum script. Args: trainable_only (bool): if True, returns only trainable parameters operations_only (bool): if True, returns only the parameters of the operations excluding parameters to observables of measurements **Example** >>> ops = [qml.RX(0.432, 0), qml.RY(0.543, 0), ... qml.CNOT((0,"a")), qml.RX(0.133, "a")] >>> qscript = QuantumScript(ops, [qml.expval(qml.Z(0))]) By default, all parameters are trainable and will be returned: >>> qscript.get_parameters() [0.432, 0.543, 0.133] Setting the trainable parameter indices will result in only the specified parameters being returned: >>> qscript.trainable_params = [1] # set the second parameter as trainable >>> qscript.get_parameters() [0.543] The ``trainable_only`` argument can be set to ``False`` to instead return all parameters: >>> qscript.get_parameters(trainable_only=False) [0.432, 0.543, 0.133] """iftrainable_only:params=[]forp_idxinself.trainable_params:par_info=self.par_info[p_idx]ifoperations_onlyandisinstance(self[par_info["op_idx"]],MeasurementProcess):continueop=par_info["op"]op_idx=par_info["p_idx"]params.append(op.data[op_idx])returnparams# If trainable_only=False, return all parameters# This is faster than the above and should be used when indexing `par_info` is not neededparams=[dforopinself.operationsfordinop.data]ifoperations_only:returnparamsforminself.measurements:ifm.obsisnotNone:params.extend(m.obs.data)returnparams
[docs]defbind_new_parameters(self,params:Sequence[TensorLike],indices:Sequence[int])->"QuantumScript":"""Create a new tape with updated parameters. This function takes a list of new parameters as input, and returns a new :class:`~.tape.QuantumScript` containing the new parameters at the provided indices, with the parameters at all other indices remaining the same. Args: params (Sequence[TensorLike]): New parameters to create the tape with. This must have the same length as ``indices``. indices (Sequence[int]): The parameter indices to update with the given parameters. The index of a parameter is defined as its index in ``tape.get_parameters()``. Returns: .tape.QuantumScript: New tape with updated parameters **Example** >>> ops = [qml.RX(0.432, 0), qml.RY(0.543, 0), ... qml.CNOT((0,"a")), qml.RX(0.133, "a")] >>> qscript = QuantumScript(ops, [qml.expval(qml.Z(0))]) A new tape can be created by passing new parameters along with the indices to be updated. To modify all parameters in the above qscript: >>> new_qscript = qscript.bind_new_parameters([0.1, 0.2, 0.3], [0, 1, 2]) >>> new_qscript.get_parameters() [0.1, 0.2, 0.3] The original ``qscript`` remains unchanged: >>> qscript.get_parameters() [0.432, 0.543, 0.133] A subset of parameters can be modified as well, defined by the parameter indices: >>> newer_qscript = new_qscript.bind_new_parameters([-0.1, 0.5], [0, 2]) >>> newer_qscript.get_parameters() [-0.1, 0.2, 0.5] """# pylint: disable=no-memberiflen(params)!=len(indices):raiseValueError("Number of provided parameters does not match number of indices")# determine the ops that need to be updatedop_indices={}forparam_idx,idxinenumerate(sorted(indices)):pinfo=self.par_info[idx]op_idx,p_idx=pinfo["op_idx"],pinfo["p_idx"]ifop_idxnotinop_indices:op_indices[op_idx]={}op_indices[op_idx][p_idx]=param_idxnew_ops=self.circuitforop_idx,p_indicesinop_indices.items():op=new_ops[op_idx]data=op.dataifisinstance(op,Operator)elseop.obs.datanew_params=[params[p_indices[i]]ifiinp_indiceselsedfori,dinenumerate(data)]ifisinstance(op,Operator):new_op=qml.ops.functions.bind_new_parameters(op,new_params)else:new_obs=qml.ops.functions.bind_new_parameters(op.obs,new_params)new_op=op.__class__(obs=new_obs)new_ops[op_idx]=new_opnew_operations=new_ops[:len(self.operations)]new_measurements=new_ops[len(self.operations):]returnself.__class__(new_operations,new_measurements,shots=self.shots,trainable_params=self.trainable_params,)
# ========================================================# MEASUREMENT SHAPE## We can extract the private static methods to a new class later# ========================================================
[docs]defshape(self,device:Union["qml.devices.Device","qml.devices.LegacyDevice"])->Union[tuple[int,...],tuple[tuple[int,...],...]]:"""Produces the output shape of the quantum script by inspecting its measurements and the device used for execution. .. note:: The computed shape is not stored because the output shape may be dependent on the device used for execution. Args: device (pennylane.devices.Device): the device that will be used for the script execution Returns: Union[tuple[int], tuple[tuple[int]]]: the output shape(s) of the quantum script result **Examples** .. code-block:: pycon >>> dev = qml.device('default.qubit', wires=2) >>> qs = QuantumScript(measurements=[qml.state()]) >>> qs.shape(dev) (4,) >>> m = [qml.state(), qml.expval(qml.Z(0)), qml.probs((0,1))] >>> qs = QuantumScript(measurements=m) >>> qs.shape(dev) ((4,), (), (4,)) """num_device_wires=len(device.wires)ifdevice.wireselselen(self.wires)defget_shape(mp,_shots):# depends on num_device_wires and self.batch_size from closurestandard_shape=mp.shape(shots=_shots,num_device_wires=num_device_wires)ifself.batch_size:return(self.batch_size,*standard_shape)returnstandard_shapeshape=[]forsinself.shotsifself.shotselse[None]:shots_shape=tuple(get_shape(mp,s)formpinself.measurements)shots_shape=shots_shape[0]iflen(shots_shape)==1elsetuple(shots_shape)shape.append(shots_shape)returntuple(shape)ifself.shots.has_partitioned_shotselseshape[0]
@propertydefnumeric_type(self)->Union[type,tuple[type,...]]:"""Returns the expected numeric type of the quantum script result by inspecting its measurements. Returns: Union[type, Tuple[type]]: The numeric type corresponding to the result type of the quantum script, or a tuple of such types if the script contains multiple measurements **Example:** .. code-block:: pycon >>> dev = qml.device('default.qubit', wires=2) >>> qs = QuantumScript(measurements=[qml.state()]) >>> qs.numeric_type complex """types=tuple(observable.numeric_typeforobservableinself.measurements)returntypes[0]iflen(types)==1elsetypes# ========================================================# Transforms: QuantumScript to QuantumScript# ========================================================
[docs]defcopy(self,copy_operations:bool=False,**update)->"QuantumScript":"""Returns a copy of the quantum script. If any attributes are defined via keyword argument, those are used on the new tape - otherwise, all attributes match the original tape. The copy is a shallow copy if `copy_operations` is False and no tape attributes are updated via keyword argument. Args: copy_operations (bool): If True, the operations are also shallow copied. Otherwise, if False, the copied operations will simply be references to the original operations; changing the parameters of one script will likewise change the parameters of all copies. If any keyword arguments are passed to update, this argument will be treated as True. Keyword Args: operations (Iterable[Operator]): An iterable of the operations to be performed. If provided, these operations will replace the copied operations on the new tape. measurements (Iterable[MeasurementProcess]): All the measurements to be performed. If provided, these measurements will replace the copied measurements on the new tape. shots (None, int, Sequence[int], ~.Shots): Number and/or batches of shots for execution. If provided, these shots will replace the copied shots on the new tape. trainable_params (None, Sequence[int]): The indices for which parameters are trainable. If provided, these parameter indices will replace the copied parameter indices on the new tape. Returns: QuantumScript : A copy of the quantum script, with modified attributes if specified by keyword argument. **Example** .. code-block:: python tape = qml.tape.QuantumScript( ops= [qml.X(0), qml.Y(1)], measurements=[qml.expval(qml.Z(0))], shots=2000) new_tape = tape.copy(measurements=[qml.expval(qml.X(1))]) >>> tape.measurements [qml.expval(qml.Z(0)] >>> new_tape.measurements [qml.expval(qml.X(1))] >>> new_tape.shots Shots(total_shots=2000, shot_vector=(ShotCopies(2000 shots x 1),)) """ifupdate:if"ops"inupdate:update["operations"]=update["ops"]forkinupdate:ifknotin["ops","operations","measurements","shots","trainable_params"]:raiseTypeError(f"{self.__class__}.copy() got an unexpected key '{k}' in update dict")ifcopy_operationsorupdate:# Perform a shallow copy of all operations in the operation and measurement# queues. The operations will continue to share data with the original script operations# unless modified._ops=update.get("operations",[copy.copy(op)foropinself.operations])_measurements=update.get("measurements",[copy.copy(op)foropinself.measurements])else:# Perform a shallow copy of the operation and measurement queues. The# operations within the queues will be references to the original script operations;# changing the original operations will always alter the operations on the copied script._ops=self.operations.copy()_measurements=self.measurements.copy()update_trainable_params="operations"inupdateor"measurements"inupdate# passing trainable_params=None will re-calculate trainable_paramsdefault_trainable_params=Noneifupdate_trainable_paramselseself.trainable_paramsnew_qscript=self.__class__(ops=_ops,measurements=_measurements,shots=update.get("shots",self.shots),trainable_params=update.get("trainable_params",default_trainable_params),)# copy cached properties when relevantnew_qscript._graph=Noneifcopy_operationsorupdateelseself._graphifnotupdate.get("operations"):# batch size may change if operations were updatednew_qscript._batch_size=self._batch_sizeifnotupdate.get("measurements"):# obs may change if measurements were updatednew_qscript._obs_sharing_wires=self._obs_sharing_wiresnew_qscript._obs_sharing_wires_id=self._obs_sharing_wires_idreturnnew_qscript
[docs]defexpand(self,depth:int=1,stop_at:Optional[Callable[[Union[Operation,MeasurementProcess]],bool]]=None,expand_measurements:bool=False,)->"QuantumScript":"""Expand all operations to a specific depth. Args: depth (int): the depth the script should be expanded stop_at (Callable): A function which accepts a queue object, and returns ``True`` if this object should *not* be expanded. If not provided, all objects that support expansion will be expanded. expand_measurements (bool): If ``True``, measurements will be expanded to basis rotations and computational basis measurements. .. seealso:: :func:`~.pennylane.devices.preprocess.decompose` for a transform that performs the same job and fits into the current transform architecture. .. warning:: This method cannot be used with a tape with non-commuting measurements, even if ``expand_measurements=False``. >>> mps = [qml.expval(qml.X(0)), qml.expval(qml.Y(0))] >>> tape = qml.tape.QuantumScript([], mps) >>> tape.expand() QuantumFunctionError: Only observables that are qubit-wise commuting Pauli words can be returned on the same wire, some of the following measurements do not commute: [expval(X(0)), expval(Y(0))] Since commutation is determined by pauli word arithmetic, non-pauli words cannot share wires with other measurements, even if they commute: >>> measurements = [qml.expval(qml.Projector([0], 0)), qml.probs(wires=0)] >>> tape = qml.tape.QuantumScript([], measurements) >>> tape.expand() QuantumFunctionError: Only observables that are qubit-wise commuting Pauli words can be returned on the same wire, some of the following measurements do not commute: [expval(Projector(array([0]), wires=[0])), probs(wires=[0])] For this reason, we recommend the use of :func:`~.pennylane.devices.preprocess.decompose` instead. .. details:: :title: Usage Details >>> ops = [qml.Permute((2,1,0), wires=(0,1,2)), qml.X(0)] >>> measurements = [qml.expval(qml.X(0))] >>> tape = qml.tape.QuantumScript(ops, measurements) >>> expanded_tape = tape.expand() >>> print(expanded_tape.draw()) 0: ─╭SWAP──Rϕ──RX──Rϕ─┤ <X> 2: ─╰SWAP─────────────┤ Specifying a depth greater than one decomposes operations multiple times. >>> expanded_tape2 = tape.expand(depth=2) >>> print(expanded_tape2.draw()) 0: ─╭●─╭X─╭●──RZ──GlobalPhase──RX──RZ──GlobalPhase─┤ <Z> 2: ─╰X─╰●─╰X──────GlobalPhase──────────GlobalPhase─┤ The ``stop_at`` callable allows the specification of terminal operations that should no longer be decomposed. In this example, the ``X`` operator is not decomposed because ``stop_at(qml.X(0)) == True``. >>> def stop_at(obj): ... return isinstance(obj, qml.X) >>> expanded_tape = tape.expand(stop_at=stop_at) >>> print(expanded_tape.draw()) 0: ─╭SWAP──X─┤ <X> 2: ─╰SWAP────┤ .. warning:: If an operator does not have a decomposition, it will not be decomposed, even if ``stop_at(obj) == False``. If you want to decompose to reach a certain gateset, you will need an extra validation pass to ensure you have reached the gateset. >>> def stop_at(obj): ... return getattr(obj, "name", "") in {"RX", "RY"} >>> tape = qml.tape.QuantumScript([qml.RZ(0.1, 0)]) >>> tape.expand(stop_at=stop_at).circuit [RZ(0.1, wires=[0])] If more than one observable exists on a wire, the diagonalizing gates will be applied and the observable will be substituted for an analogous combination of ``qml.Z`` operators. This will happen even if ``expand_measurements=False``. >>> mps = [qml.expval(qml.X(0)), qml.expval(qml.X(0) @ qml.X(1))] >>> tape = qml.tape.QuantumScript([], mps) >>> expanded_tape = tape.expand() >>> print(expanded_tape.draw()) 0: ──RY─┤ <Z> ╭<Z@Z> 1: ──RY─┤ ╰<Z@Z> Setting ``expand_measurements=True`` applies any diagonalizing gates and converts the measurement into a wires+eigvals representation. .. warning:: Many components of PennyLane do not support the wires + eigvals representation. Setting ``expand_measurements=True`` should be used with extreme caution. >>> tape = qml.tape.QuantumScript([], [qml.expval(qml.X(0))]) >>> tape.expand(expand_measurements=True).circuit [H(0), expval(eigvals=[ 1. -1.], wires=[0])] """returnqml.tape.tape.expand_tape(self,depth=depth,stop_at=stop_at,expand_measurements=expand_measurements)
[docs]defadjoint(self)->"QuantumScript":"""Create a quantum script that is the adjoint of this one. Adjointed quantum scripts are the conjugated and transposed version of the original script. Adjointed ops are equivalent to the inverted operation for unitary gates. Returns: ~.QuantumScript: the adjointed script """ops=self.operations[self.num_preps:]prep=self.operations[:self.num_preps]withqml.QueuingManager.stop_recording():ops_adj=[qml.adjoint(op,lazy=False)foropinreversed(ops)]returnself.__class__(ops=prep+ops_adj,measurements=self.measurements,shots=self.shots)
# ========================================================# Transforms: QuantumScript to Information# ========================================================@propertydefgraph(self)->"qml.CircuitGraph":"""Returns a directed acyclic graph representation of the recorded quantum circuit: >>> ops = [qml.StatePrep([0, 1], 0), qml.RX(0.432, 0)] >>> qscript = QuantumScript(ops, [qml.expval(qml.Z(0))]) >>> qscript.graph <pennylane.circuit_graph.CircuitGraph object at 0x7fcc0433a690> Note that the circuit graph is only constructed once, on first call to this property, and cached for future use. Returns: .CircuitGraph: the circuit graph object """ifself._graphisNone:self._graph=qml.CircuitGraph(self.operations,self.measurements,self.wires,self.par_info,self.trainable_params,)returnself._graph@propertydefspecs(self)->SpecsDict[str,Any]:"""Resource information about a quantum circuit. Returns: dict[str, Union[defaultdict,int]]: dictionaries that contain quantum script specifications **Example** >>> ops = [qml.Hadamard(0), qml.RX(0.26, 1), qml.CNOT((1,0)), ... qml.Rot(1.8, -2.7, 0.2, 0), qml.Hadamard(1), qml.CNOT((0, 1))] >>> qscript = QuantumScript(ops, [qml.expval(qml.Z(0) @ qml.Z(1))]) Asking for the specs produces a dictionary of useful information about the circuit: >>> qscript.specs['num_observables'] 1 >>> qscript.specs['gate_sizes'] defaultdict(<class 'int'>, {1: 4, 2: 2}) >>> print(qscript.specs['resources']) num_wires: 2 num_gates: 6 depth: 4 shots: Shots(total=None) gate_types: {'Hadamard': 2, 'RX': 1, 'CNOT': 2, 'Rot': 1} gate_sizes: {1: 4, 2: 2} """# pylint: disable=protected-accessifself._specsisNone:resources=qml.resource.resource._count_resources(self)algo_errors=qml.resource.error._compute_algo_error(self)self._specs=SpecsDict({"resources":resources,"errors":algo_errors,"num_observables":len(self.observables),"num_trainable_params":self.num_params,})returnself._specs# pylint: disable=too-many-arguments, too-many-positional-arguments
[docs]defdraw(self,wire_order:Optional[Iterable[Hashable]]=None,show_all_wires:bool=False,decimals:Optional[int]=None,max_length:int=100,show_matrices:bool=True,)->str:"""Draw the quantum script as a circuit diagram. See :func:`~.drawer.tape_text` for more information. Args: wire_order (Sequence[Any]): the order (from top to bottom) to print the wires of the circuit show_all_wires (bool): If True, all wires, including empty wires, are printed. decimals (int): How many decimal points to include when formatting operation parameters. Default ``None`` will omit parameters from operation labels. max_length (Int) : Maximum length of a individual line. After this length, the diagram will begin anew beneath the previous lines. show_matrices=True (bool): show matrix valued parameters below all circuit diagrams Returns: str: the circuit representation of the quantum script """returnqml.drawer.tape_text(self,wire_order=wire_order,show_all_wires=show_all_wires,decimals=decimals,max_length=max_length,show_matrices=show_matrices,)
[docs]defto_openqasm(self,wires:Optional[WiresLike]=None,rotations:bool=True,measure_all:bool=True,precision:Optional[int]=None,)->str:"""Serialize the circuit as an OpenQASM 2.0 program. Measurements are assumed to be performed on all qubits in the computational basis. An optional ``rotations`` argument can be provided so that output of the OpenQASM circuit is diagonal in the eigenbasis of the quantum script's observables. The measurement outputs can be restricted to only those specified in the script by setting ``measure_all=False``. .. note:: The serialized OpenQASM program assumes that gate definitions in ``qelib1.inc`` are available. Args: wires (Wires or None): the wires to use when serializing the circuit rotations (bool): in addition to serializing user-specified operations, also include the gates that diagonalize the measured wires such that they are in the eigenbasis of the circuit observables. measure_all (bool): whether to perform a computational basis measurement on all qubits or just those specified in the script precision (int): decimal digits to display for parameters Returns: str: OpenQASM serialization of the circuit """wires=wiresorself.wires# add the QASM headersqasm_str='OPENQASM 2.0;\ninclude "qelib1.inc";\n'ifself.num_wires==0:# empty circuitreturnqasm_str# create the quantum and classical registersqasm_str+=f"qreg q[{len(wires)}];\n"qasm_str+=f"creg c[{len(wires)}];\n"# get the user applied circuit operations without interface information[transformed_tape],_=qml.transforms.convert_to_numpy_parameters(self)operations=transformed_tape.operationsifrotations:# if requested, append diagonalizing gates corresponding# to circuit observablesoperations+=self.diagonalizing_gates# decompose the queue# pylint: disable=no-memberjust_ops=QuantumScript(operations)operations=just_ops.expand(depth=10,stop_at=lambdaobj:obj.nameinOPENQASM_GATES).operations# create the QASM code representing the operationsforopinoperations:try:gate=OPENQASM_GATES[op.name]exceptKeyErrorase:raiseValueError(f"Operation {op.name} not supported by the QASM serializer")fromewire_labels=",".join([f"q[{wires.index(w)}]"forwinop.wires.tolist()])params=""ifop.num_params>0:# If the operation takes parameters, construct a string# with parameter values.ifprecisionisnotNone:params="("+",".join([f"{p:.{precision}}"forpinop.parameters])+")"else:# use default precisionparams="("+",".join([str(p)forpinop.parameters])+")"qasm_str+=f"{gate}{params}{wire_labels};\n"# apply computational basis measurements to each quantum register# NOTE: This is not strictly necessary, we could inspect self.observables,# and then only measure wires which are requested by the user. However,# some devices which consume QASM require all registers be measured, so# measure all wires by default to be safe.ifmeasure_all:forwireinrange(len(wires)):qasm_str+=f"measure q[{wire}] -> c[{wire}];\n"else:measured_wires=Wires.all_wires([m.wiresforminself.measurements])forwinmeasured_wires:wire_indx=self.wires.index(w)qasm_str+=f"measure q[{wire_indx}] -> c[{wire_indx}];\n"returnqasm_str
[docs]@classmethoddeffrom_queue(cls:type[QS],queue:qml.queuing.AnnotatedQueue,shots:Optional[ShotsLike]=None)->QS:"""Construct a QuantumScript from an AnnotatedQueue."""returncls(*process_queue(queue),shots=shots)
[docs]defmap_to_standard_wires(self)->"QuantumScript":""" Map a circuit's wires such that they are in a standard order. If no mapping is required, the unmodified circuit is returned. Returns: QuantumScript: The circuit with wires in the standard order The standard order is defined by the operator wires being increasing integers starting at zero, to match array indices. If there are any measurement wires that are not in any operations, those will be mapped to higher values. **Example:** >>> circuit = qml.tape.QuantumScript([qml.X("a")], [qml.expval(qml.Z("b"))]) >>> circuit.map_to_standard_wires().circuit [X(0), expval(Z(1))] If any measured wires are not in any operations, they will be mapped last: >>> circuit = qml.tape.QuantumScript([qml.X(1)], [qml.probs(wires=[0, 1])]) >>> circuit.map_to_standard_wires().circuit [X(0), probs(wires=[1, 0])] If no wire-mapping is needed, then the returned circuit *is* the inputted circuit: >>> circuit = qml.tape.QuantumScript([qml.X(0)], [qml.expval(qml.Z(1))]) >>> circuit.map_to_standard_wires() is circuit True """op_wires=Wires.all_wires(op.wiresforopinself.operations)meas_wires=Wires.all_wires(mp.wiresformpinself.measurements)num_op_wires=len(op_wires)meas_only_wires=set(meas_wires)-set(op_wires)ifset(op_wires)==set(range(num_op_wires))andmeas_only_wires==set(range(num_op_wires,num_op_wires+len(meas_only_wires))):returnselfwire_map={w:ifori,winenumerate(op_wires+meas_only_wires)}tapes,fn=qml.map_wires(self,wire_map)returnfn(tapes)
# TODO: Use "ParamSpecs" when min Python version is 3.10
[docs]defmake_qscript(fn:Callable[...,Any],shots:Optional[ShotsLike]=None)->Callable[...,QuantumScript]:"""Returns a function that generates a qscript from a quantum function without any operation queuing taking place. This is useful when you would like to manipulate or transform the qscript created by a quantum function without evaluating it. Args: fn (function): the quantum function to generate the qscript from shots (None, int, Sequence[int], ~.Shots): number and/or batches of executions Returns: function: The returned function takes the same arguments as the quantum function. When called, it returns the generated quantum script without any queueing occurring. **Example** Consider the following quantum function: .. code-block:: python def qfunc(x): qml.Hadamard(wires=0) qml.CNOT(wires=[0, 1]) qml.RX(x, wires=0) We can use ``make_qscript`` to extract the qscript generated by this quantum function, without any of the operations being queued by any existing queuing contexts: >>> with qml.queuing.AnnotatedQueue() as active_queue: ... _ = qml.RY(1.0, wires=0) ... qs = make_qscript(qfunc)(0.5) >>> qs.operations [H(0), CNOT(wires=[0, 1]), RX(0.5, wires=[0])] Note that the currently recording queue did not queue any of these quantum operations: >>> active_queue.queue [RY(1.0, wires=[0])] """defwrapper(*args,**kwargs):withAnnotatedQueue()asq:fn(*args,**kwargs)returnQuantumScript.from_queue(q,shots)returnwrapper
QuantumScriptBatch=Sequence[QuantumScript]QuantumScriptOrBatch=Union[QuantumScript,QuantumScriptBatch]register_pytree(QuantumScript,QuantumScript._flatten,QuantumScript._unflatten)def_get_base_obs(observables):overlapping_ops_observables=[]whileany(isinstance(o,(qml.ops.CompositeOp,qml.ops.SymbolicOp))foroinobservables):new_obs=[]forobservableinobservables:ifisinstance(observable,qml.ops.CompositeOp):ifany(len(o)>1foroinobservable.overlapping_ops):overlapping_ops_observables.append(observable)else:new_obs.extend(observable.operands)elifisinstance(observable,qml.ops.SymbolicOp):new_obs.append(observable.base)else:new_obs.append(observable)observables=new_obs# removes duplicates from list without disrupting order - basically an ordered setreturnlist(dict.fromkeys(observables+overlapping_ops_observables))